diff --git a/CODEOWNERS b/CODEOWNERS index df77ea8d5b0..9d88b498845 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -91,6 +91,8 @@ homeassistant/components/aurora/* @djtimca tests/components/aurora/* @djtimca homeassistant/components/aurora_abb_powerone/* @davet2001 tests/components/aurora_abb_powerone/* @davet2001 +homeassistant/components/aussie_broadband/* @nickw444 @Bre77 +tests/components/aussie_broadband/* @nickw444 @Bre77 homeassistant/components/auth/* @home-assistant/core tests/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py new file mode 100644 index 00000000000..d967450788f --- /dev/null +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -0,0 +1,84 @@ +"""The Aussie Broadband integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging + +from aiohttp import ClientError +from aussiebb.asyncio import AussieBB, AuthenticationException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_SERVICES, DEFAULT_UPDATE_INTERVAL, DOMAIN, SERVICE_ID + +_LOGGER = logging.getLogger(__name__) +PLATFORMS = [Platform.SENSOR] + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Aussie Broadband from a config entry.""" + # Login to the Aussie Broadband API and retrieve the current service list + client = AussieBB( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + async_get_clientsession(hass), + ) + try: + await client.login() + all_services = await client.get_services() + except AuthenticationException as exc: + raise ConfigEntryAuthFailed() from exc + except ClientError as exc: + raise ConfigEntryNotReady() from exc + + # Filter the service list to those that are enabled in options + services = [ + s for s in all_services if str(s["service_id"]) in entry.options[CONF_SERVICES] + ] + + # Create an appropriate refresh function + def update_data_factory(service_id): + async def async_update_data(): + return await client.get_usage(service_id) + + return async_update_data + + # Initiate a Data Update Coordinator for each service + for service in services: + service["coordinator"] = DataUpdateCoordinator( + hass, + _LOGGER, + name=service["service_id"], + update_interval=timedelta(minutes=DEFAULT_UPDATE_INTERVAL), + update_method=update_data_factory(service[SERVICE_ID]), + ) + await service["coordinator"].async_config_entry_first_refresh() + + # Setup the integration + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "client": client, + "services": services, + } + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + + return True + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Reload to update options.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload the config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/aussie_broadband/config_flow.py b/homeassistant/components/aussie_broadband/config_flow.py new file mode 100644 index 00000000000..c18e808e6ad --- /dev/null +++ b/homeassistant/components/aussie_broadband/config_flow.py @@ -0,0 +1,184 @@ +"""Config flow for Aussie Broadband integration.""" +from __future__ import annotations + +from typing import Any + +from aiohttp import ClientError +from aussiebb.asyncio import AussieBB, AuthenticationException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import CONF_SERVICES, DOMAIN, SERVICE_ID + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Aussie Broadband.""" + + VERSION = 1 + + def __init__(self): + """Initialize the config flow.""" + self.data: dict = {} + self.options: dict = {CONF_SERVICES: []} + self.services: list[dict[str]] = [] + self.client: AussieBB | None = None + self._reauth_username: str | None = None + + async def async_auth(self, user_input: dict[str, str]) -> dict[str, str] | None: + """Reusable Auth Helper.""" + self.client = AussieBB( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + async_get_clientsession(self.hass), + ) + try: + await self.client.login() + return None + except AuthenticationException: + return {"base": "invalid_auth"} + except ClientError: + return {"base": "cannot_connect"} + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] | None = None + if user_input is not None: + if not (errors := await self.async_auth(user_input)): + await self.async_set_unique_id(user_input[CONF_USERNAME].lower()) + self._abort_if_unique_id_configured() + + self.data = user_input + self.services = await self.client.get_services() # type: ignore[union-attr] + + if not self.services: + return self.async_abort(reason="no_services_found") + + if len(self.services) == 1: + return self.async_create_entry( + title=self.data[CONF_USERNAME], + data=self.data, + options={CONF_SERVICES: [str(self.services[0][SERVICE_ID])]}, + ) + + # Account has more than one service, select service to add + return await self.async_step_service() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + async def async_step_service( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the optional service selection step.""" + if user_input is not None: + return self.async_create_entry( + title=self.data[CONF_USERNAME], data=self.data, options=user_input + ) + + service_options = {str(s[SERVICE_ID]): s["description"] for s in self.services} + return self.async_show_form( + step_id="service", + data_schema=vol.Schema( + { + vol.Required( + CONF_SERVICES, default=list(service_options.keys()) + ): cv.multi_select(service_options) + } + ), + errors=None, + ) + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauth.""" + errors: dict[str, str] | None = None + if user_input and user_input.get(CONF_USERNAME): + self._reauth_username = user_input[CONF_USERNAME] + + elif self._reauth_username and user_input and user_input.get(CONF_PASSWORD): + data = { + CONF_USERNAME: self._reauth_username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + + if not (errors := await self.async_auth(data)): + entry = await self.async_set_unique_id(self._reauth_username.lower()) + if entry: + self.hass.config_entries.async_update_entry( + entry, + data=data, + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=self._reauth_username, data=data) + + return self.async_show_form( + step_id="reauth", + description_placeholders={"username": self._reauth_username}, + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Options flow for picking services.""" + + 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): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + if self.config_entry.state != config_entries.ConfigEntryState.LOADED: + return self.async_abort(reason="unknown") + data = self.hass.data[DOMAIN][self.config_entry.entry_id] + try: + services = await data["client"].get_services() + except AuthenticationException: + return self.async_abort(reason="invalid_auth") + except ClientError: + return self.async_abort(reason="cannot_connect") + service_options = {str(s[SERVICE_ID]): s["description"] for s in services} + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required( + CONF_SERVICES, + default=self.config_entry.options.get(CONF_SERVICES), + ): cv.multi_select(service_options), + } + ), + ) diff --git a/homeassistant/components/aussie_broadband/const.py b/homeassistant/components/aussie_broadband/const.py new file mode 100644 index 00000000000..5747ee86fea --- /dev/null +++ b/homeassistant/components/aussie_broadband/const.py @@ -0,0 +1,5 @@ +"""Constants for the Aussie Broadband integration.""" +DEFAULT_UPDATE_INTERVAL = 30 +DOMAIN = "aussie_broadband" +SERVICE_ID = "service_id" +CONF_SERVICES = "services" diff --git a/homeassistant/components/aussie_broadband/manifest.json b/homeassistant/components/aussie_broadband/manifest.json new file mode 100644 index 00000000000..fb7ce828324 --- /dev/null +++ b/homeassistant/components/aussie_broadband/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "aussie_broadband", + "name": "Aussie Broadband", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/aussie_broadband", + "requirements": [ + "pyaussiebb==0.0.9" + ], + "codeowners": [ + "@nickw444", + "@Bre77" + ], + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/homeassistant/components/aussie_broadband/sensor.py b/homeassistant/components/aussie_broadband/sensor.py new file mode 100644 index 00000000000..239bf42cebe --- /dev/null +++ b/homeassistant/components/aussie_broadband/sensor.py @@ -0,0 +1,140 @@ +"""Support for Aussie Broadband metric sensors.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.sensor import ( + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DATA_KILOBYTES, DATA_MEGABYTES, TIME_DAYS +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, SERVICE_ID + +SENSOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = ( + # Internet Services sensors + SensorEntityDescription( + key="usedMb", + name="Data Used", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:network", + ), + SensorEntityDescription( + key="downloadedMb", + name="Downloaded", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:download-network", + ), + SensorEntityDescription( + key="uploadedMb", + name="Uploaded", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=DATA_MEGABYTES, + icon="mdi:upload-network", + ), + # Mobile Phone Services sensors + SensorEntityDescription( + key="national", + name="National Calls", + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:phone", + ), + SensorEntityDescription( + key="mobile", + name="Mobile Calls", + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:phone", + ), + SensorEntityDescription( + key="international", + name="International Calls", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:phone-plus", + ), + SensorEntityDescription( + key="sms", + name="SMS Sent", + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:message-processing", + ), + SensorEntityDescription( + key="internet", + name="Data Used", + state_class=SensorStateClass.TOTAL_INCREASING, + native_unit_of_measurement=DATA_KILOBYTES, + icon="mdi:network", + ), + SensorEntityDescription( + key="voicemail", + name="Voicemail Calls", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:phone", + ), + SensorEntityDescription( + key="other", + name="Other Calls", + entity_registry_enabled_default=False, + state_class=SensorStateClass.TOTAL_INCREASING, + icon="mdi:phone", + ), + # Generic sensors + SensorEntityDescription( + key="daysTotal", + name="Billing Cycle Length", + native_unit_of_measurement=TIME_DAYS, + icon="mdi:calendar-range", + ), + SensorEntityDescription( + key="daysRemaining", + name="Billing Cycle Remaining", + native_unit_of_measurement=TIME_DAYS, + icon="mdi:calendar-clock", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +): + """Set up the Aussie Broadband sensor platform from a config entry.""" + + async_add_entities( + [ + AussieBroadandSensorEntity(service, description) + for service in hass.data[DOMAIN][entry.entry_id]["services"] + for description in SENSOR_DESCRIPTIONS + if description.key in service["coordinator"].data + ] + ) + return True + + +class AussieBroadandSensorEntity(CoordinatorEntity, SensorEntity): + """Base class for Aussie Broadband metric sensors.""" + + def __init__( + self, service: dict[str, Any], description: SensorEntityDescription + ) -> None: + """Initialize the sensor.""" + super().__init__(service["coordinator"]) + self.entity_description = description + self._attr_unique_id = f"{service[SERVICE_ID]}:{description.key}" + self._attr_name = f"{service['name']} {description.name}" + + @property + def native_value(self): + """Return the state of the sensor.""" + if self.entity_description.key == "internet": + return self.coordinator.data[self.entity_description.key]["kbytes"] + if self.entity_description.key in ("national", "mobile", "sms"): + return self.coordinator.data[self.entity_description.key]["calls"] + return self.coordinator.data[self.entity_description.key] diff --git a/homeassistant/components/aussie_broadband/strings.json b/homeassistant/components/aussie_broadband/strings.json new file mode 100644 index 00000000000..42ebcbbdbe8 --- /dev/null +++ b/homeassistant/components/aussie_broadband/strings.json @@ -0,0 +1,50 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "service": { + "title": "Select Services", + "data": { + "services": "Services" + } + }, + "reauth": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Update password for {username}", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "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_account%]", + "no_services_found": "No services were found for this account", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + } + }, + "options": { + "step": { + "init": { + "title": "Select Services", + "data": { + "services": "Services" + } + } + }, + "abort": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 60c4e32fad3..06225860376 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -34,6 +34,7 @@ FLOWS = [ "august", "aurora", "aurora_abb_powerone", + "aussie_broadband", "awair", "axis", "azure_devops", diff --git a/requirements_all.txt b/requirements_all.txt index 39b4a1853ef..2d8ddca8e48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1403,6 +1403,9 @@ pyatome==0.1.1 # homeassistant.components.apple_tv pyatv==0.9.8 +# homeassistant.components.aussie_broadband +pyaussiebb==0.0.9 + # homeassistant.components.balboa pybalboa==0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a37e747289b..84eb03bd6c9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -877,6 +877,9 @@ pyatmo==6.2.2 # homeassistant.components.apple_tv pyatv==0.9.8 +# homeassistant.components.aussie_broadband +pyaussiebb==0.0.9 + # homeassistant.components.balboa pybalboa==0.13 diff --git a/tests/components/aussie_broadband/__init__.py b/tests/components/aussie_broadband/__init__.py new file mode 100644 index 00000000000..ef618b1f701 --- /dev/null +++ b/tests/components/aussie_broadband/__init__.py @@ -0,0 +1 @@ +"""Tests for the Aussie Broadband integration.""" diff --git a/tests/components/aussie_broadband/common.py b/tests/components/aussie_broadband/common.py new file mode 100644 index 00000000000..abb4bce042d --- /dev/null +++ b/tests/components/aussie_broadband/common.py @@ -0,0 +1,58 @@ +"""Aussie Broadband common helpers for tests.""" +from unittest.mock import patch + +from homeassistant.components.aussie_broadband.const import ( + CONF_SERVICES, + DOMAIN as AUSSIE_BROADBAND_DOMAIN, +) +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME + +from tests.common import MockConfigEntry + +FAKE_SERVICES = [ + { + "service_id": "12345678", + "description": "Fake ABB NBN Service", + "type": "NBN", + "name": "NBN", + }, + { + "service_id": "87654321", + "description": "Fake ABB Mobile Service", + "type": "PhoneMobile", + "name": "Mobile", + }, +] + +FAKE_DATA = { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", +} + + +async def setup_platform(hass, platforms=[], side_effect=None, usage={}): + """Set up the Aussie Broadband platform.""" + mock_entry = MockConfigEntry( + domain=AUSSIE_BROADBAND_DOMAIN, + data=FAKE_DATA, + options={CONF_SERVICES: ["12345678", "87654321"], CONF_SCAN_INTERVAL: 30}, + ) + mock_entry.add_to_hass(hass) + + with patch("homeassistant.components.aussie_broadband.PLATFORMS", platforms), patch( + "aussiebb.asyncio.AussieBB.__init__", return_value=None + ), patch( + "aussiebb.asyncio.AussieBB.login", + return_value=True, + side_effect=side_effect, + ), patch( + "aussiebb.asyncio.AussieBB.get_services", + return_value=FAKE_SERVICES, + side_effect=side_effect, + ), patch( + "aussiebb.asyncio.AussieBB.get_usage", return_value=usage + ): + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/aussie_broadband/test_config_flow.py b/tests/components/aussie_broadband/test_config_flow.py new file mode 100644 index 00000000000..7e919636b09 --- /dev/null +++ b/tests/components/aussie_broadband/test_config_flow.py @@ -0,0 +1,322 @@ +"""Test the Aussie Broadband config flow.""" +from unittest.mock import patch + +from aiohttp import ClientConnectionError +from aussiebb.asyncio import AuthenticationException + +from homeassistant import config_entries, setup +from homeassistant.components.aussie_broadband.const import CONF_SERVICES, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from .common import FAKE_DATA, FAKE_SERVICES, setup_platform + +TEST_USERNAME = FAKE_DATA[CONF_USERNAME] +TEST_PASSWORD = FAKE_DATA[CONF_PASSWORD] + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result1["type"] == RESULT_TYPE_FORM + assert result1["errors"] is None + + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", return_value=True + ), patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + FAKE_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == TEST_USERNAME + assert result2["data"] == FAKE_DATA + assert result2["options"] == {CONF_SERVICES: ["12345678"]} + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_already_configured(hass: HomeAssistant) -> None: + """Test already configured.""" + # Setup an entry + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", return_value=True + ), patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + await hass.config_entries.flow.async_configure( + result1["flow_id"], + FAKE_DATA, + ) + await hass.async_block_till_done() + + # Test Already configured + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", return_value=True + ), patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + FAKE_DATA, + ) + await hass.async_block_till_done() + + assert result4["type"] == RESULT_TYPE_ABORT + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_no_services(hass: HomeAssistant) -> None: + """Test when there are no services.""" + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result1["type"] == RESULT_TYPE_FORM + assert result1["errors"] is None + + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", return_value=True + ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[]), patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + FAKE_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "no_services_found" + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_multiple_services(hass: HomeAssistant) -> None: + """Test the config flow with multiple services.""" + 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("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", return_value=True + ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=FAKE_SERVICES): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + FAKE_DATA, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "service" + assert result2["errors"] is None + + with patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_SERVICES: [FAKE_SERVICES[1]["service_id"]]}, + ) + await hass.async_block_till_done() + + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == TEST_USERNAME + assert result3["data"] == FAKE_DATA + assert result3["options"] == { + CONF_SERVICES: [FAKE_SERVICES[1]["service_id"]], + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test invalid auth is handled.""" + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException() + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + FAKE_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_network_issue(hass: HomeAssistant) -> None: + """Test network issues are handled.""" + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", side_effect=ClientConnectionError() + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + FAKE_DATA, + ) + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth(hass: HomeAssistant) -> None: + """Test reauth flow.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + + # Test reauth but the entry doesn't exist + result1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=FAKE_DATA + ) + + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", return_value=True + ), patch( + "aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]] + ), patch( + "homeassistant.components.aussie_broadband.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result1["flow_id"], + { + CONF_PASSWORD: TEST_PASSWORD, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == TEST_USERNAME + assert result2["data"] == FAKE_DATA + + # Test failed reauth + result5 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=FAKE_DATA, + ) + assert result5["step_id"] == "reauth" + + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", side_effect=AuthenticationException() + ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]]): + + result6 = await hass.config_entries.flow.async_configure( + result5["flow_id"], + { + CONF_PASSWORD: "test-wrongpassword", + }, + ) + await hass.async_block_till_done() + + assert result6["step_id"] == "reauth" + + # Test successful reauth + with patch("aussiebb.asyncio.AussieBB.__init__", return_value=None), patch( + "aussiebb.asyncio.AussieBB.login", return_value=True + ), patch("aussiebb.asyncio.AussieBB.get_services", return_value=[FAKE_SERVICES[0]]): + + result7 = await hass.config_entries.flow.async_configure( + result6["flow_id"], + { + CONF_PASSWORD: "test-newpassword", + }, + ) + await hass.async_block_till_done() + + assert result7["type"] == "abort" + assert result7["reason"] == "reauth_successful" + + +async def test_options_flow(hass): + """Test options flow.""" + entry = await setup_platform(hass) + + with patch("aussiebb.asyncio.AussieBB.get_services", return_value=FAKE_SERVICES): + + result1 = await hass.config_entries.options.async_init(entry.entry_id) + assert result1["type"] == RESULT_TYPE_FORM + assert result1["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result1["flow_id"], + user_input={CONF_SERVICES: []}, + ) + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert entry.options == {CONF_SERVICES: []} + + +async def test_options_flow_auth_failure(hass): + """Test options flow with auth failure.""" + + entry = await setup_platform(hass) + + with patch( + "aussiebb.asyncio.AussieBB.get_services", side_effect=AuthenticationException() + ): + + result1 = await hass.config_entries.options.async_init(entry.entry_id) + assert result1["type"] == RESULT_TYPE_ABORT + assert result1["reason"] == "invalid_auth" + + +async def test_options_flow_network_failure(hass): + """Test options flow with connectivity failure.""" + + entry = await setup_platform(hass) + + with patch( + "aussiebb.asyncio.AussieBB.get_services", side_effect=ClientConnectionError() + ): + + result1 = await hass.config_entries.options.async_init(entry.entry_id) + assert result1["type"] == RESULT_TYPE_ABORT + assert result1["reason"] == "cannot_connect" + + +async def test_options_flow_not_loaded(hass): + """Test the options flow aborts when the entry has unloaded due to a reauth.""" + + entry = await setup_platform(hass) + + with patch( + "aussiebb.asyncio.AussieBB.get_services", side_effect=AuthenticationException() + ): + entry.state = config_entries.ConfigEntryState.NOT_LOADED + result1 = await hass.config_entries.options.async_init(entry.entry_id) + assert result1["type"] == RESULT_TYPE_ABORT + assert result1["reason"] == "unknown" diff --git a/tests/components/aussie_broadband/test_init.py b/tests/components/aussie_broadband/test_init.py new file mode 100644 index 00000000000..9e31aa9b737 --- /dev/null +++ b/tests/components/aussie_broadband/test_init.py @@ -0,0 +1,35 @@ +"""Test the Aussie Broadband init.""" +from unittest.mock import patch + +from aiohttp import ClientConnectionError +from aussiebb.asyncio import AuthenticationException + +from homeassistant import data_entry_flow +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from .common import setup_platform + + +async def test_unload(hass: HomeAssistant) -> None: + """Test unload.""" + entry = await setup_platform(hass) + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_auth_failure(hass: HomeAssistant) -> None: + """Test init with an authentication failure.""" + with patch( + "homeassistant.components.aussie_broadband.config_flow.ConfigFlow.async_step_reauth", + return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, + ) as mock_async_step_reauth: + await setup_platform(hass, side_effect=AuthenticationException()) + mock_async_step_reauth.assert_called_once() + + +async def test_net_failure(hass: HomeAssistant) -> None: + """Test init with a network failure.""" + entry = await setup_platform(hass, side_effect=ClientConnectionError()) + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/aussie_broadband/test_sensor.py b/tests/components/aussie_broadband/test_sensor.py new file mode 100644 index 00000000000..30fac808a27 --- /dev/null +++ b/tests/components/aussie_broadband/test_sensor.py @@ -0,0 +1,50 @@ +"""Aussie Broadband sensor platform tests.""" +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + +from .common import setup_platform + +MOCK_NBN_USAGE = { + "usedMb": 54321, + "downloadedMb": 50000, + "uploadedMb": 4321, + "daysTotal": 28, + "daysRemaining": 25, +} + +MOCK_MOBILE_USAGE = { + "national": {"calls": 1, "cost": 0}, + "mobile": {"calls": 2, "cost": 0}, + "international": {"calls": 3, "cost": 0}, + "sms": {"calls": 4, "cost": 0}, + "internet": {"kbytes": 512, "cost": 0}, + "voicemail": {"calls": 6, "cost": 0}, + "other": {"calls": 7, "cost": 0}, + "daysTotal": 31, + "daysRemaining": 30, + "historical": [], +} + + +async def test_nbn_sensor_states(hass): + """Tests that the sensors are correct.""" + + await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_NBN_USAGE) + + assert hass.states.get("sensor.nbn_data_used").state == "54321" + assert hass.states.get("sensor.nbn_downloaded").state == "50000" + assert hass.states.get("sensor.nbn_uploaded").state == "4321" + assert hass.states.get("sensor.nbn_billing_cycle_length").state == "28" + assert hass.states.get("sensor.nbn_billing_cycle_remaining").state == "25" + + +async def test_phone_sensor_states(hass): + """Tests that the sensors are correct.""" + + await setup_platform(hass, [SENSOR_DOMAIN], usage=MOCK_MOBILE_USAGE) + + assert hass.states.get("sensor.mobile_national_calls").state == "1" + assert hass.states.get("sensor.mobile_mobile_calls").state == "2" + assert hass.states.get("sensor.mobile_sms_sent").state == "4" + assert hass.states.get("sensor.mobile_data_used").state == "512" + assert hass.states.get("sensor.mobile_billing_cycle_length").state == "31" + assert hass.states.get("sensor.mobile_billing_cycle_remaining").state == "30"