diff --git a/.coveragerc b/.coveragerc index 72021c60ac1..f5f22096115 100644 --- a/.coveragerc +++ b/.coveragerc @@ -641,6 +641,12 @@ omit = homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/joaoapps_join/* + homeassistant/components/juicenet/__init__.py + homeassistant/components/juicenet/device.py + homeassistant/components/juicenet/entity.py + homeassistant/components/juicenet/number.py + homeassistant/components/juicenet/sensor.py + homeassistant/components/juicenet/switch.py homeassistant/components/justnimbus/coordinator.py homeassistant/components/justnimbus/entity.py homeassistant/components/justnimbus/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index d4d1e70388f..15af941984b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -669,6 +669,8 @@ build.json @home-assistant/supervisor /tests/components/jellyfin/ @j-stienstra @ctalkington /homeassistant/components/jewish_calendar/ @tsvi /tests/components/jewish_calendar/ @tsvi +/homeassistant/components/juicenet/ @jesserockz +/tests/components/juicenet/ @jesserockz /homeassistant/components/justnimbus/ @kvanzuijlen /tests/components/justnimbus/ @kvanzuijlen /homeassistant/components/jvc_projector/ @SteveEasley @msavazzi diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 820f0d1fcc0..bcefe763e15 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,37 +1,107 @@ """The JuiceNet integration.""" -from __future__ import annotations +from datetime import timedelta +import logging -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +import aiohttp +from pyjuicenet import Api, TokenError +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -DOMAIN = "juicenet" +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .device import JuiceNetApi + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] + +CONFIG_SCHEMA = vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, + ), + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the JuiceNet component.""" + conf = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up JuiceNet from a config entry.""" - ir.async_create_issue( + + config = entry.data + + session = async_get_clientsession(hass) + + access_token = config[CONF_ACCESS_TOKEN] + api = Api(access_token, session) + + juicenet = JuiceNetApi(api) + + try: + await juicenet.setup() + except TokenError as error: + _LOGGER.error("JuiceNet Error %s", error) + return False + except aiohttp.ClientError as error: + _LOGGER.error("Could not reach the JuiceNet API %s", error) + raise ConfigEntryNotReady from error + + if not juicenet.devices: + _LOGGER.error("No JuiceNet devices found for this account") + return False + _LOGGER.info("%d JuiceNet device(s) found", len(juicenet.devices)) + + async def async_update_data(): + """Update all device states from the JuiceNet API.""" + for device in juicenet.devices: + await device.update_state(True) + return True + + coordinator = DataUpdateCoordinator( hass, - DOMAIN, - DOMAIN, - is_fixable=False, - severity=ir.IssueSeverity.ERROR, - translation_key="integration_removed", - translation_placeholders={ - "entries": "/config/integrations/integration/juicenet", - }, + _LOGGER, + name="JuiceNet", + update_method=async_update_data, + update_interval=timedelta(seconds=30), ) + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + JUICENET_API: juicenet, + JUICENET_COORDINATOR: coordinator, + } + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if all( - config_entry.state is ConfigEntryState.NOT_LOADED - for config_entry in hass.config_entries.async_entries(DOMAIN) - if config_entry.entry_id != entry.entry_id - ): - ir.async_delete_issue(hass, DOMAIN, DOMAIN) - - return True + 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/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py index 1385aa2eca8..743e4098a9a 100644 --- a/homeassistant/components/juicenet/config_flow.py +++ b/homeassistant/components/juicenet/config_flow.py @@ -1,11 +1,78 @@ """Config flow for JuiceNet integration.""" +import logging +import aiohttp +from pyjuicenet import Api, TokenError +import voluptuous as vol + +from homeassistant import core, exceptions from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession -from . import DOMAIN +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + juicenet = Api(data[CONF_ACCESS_TOKEN], session) + + try: + await juicenet.get_devices() + except TokenError as error: + _LOGGER.error("Token Error %s", error) + raise InvalidAuth from error + except aiohttp.ClientError as error: + _LOGGER.error("Error connecting %s", error) + raise CannotConnect from error + + # Return info that you want to store in the config entry. + return {"title": "JuiceNet"} class JuiceNetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for JuiceNet.""" VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN]) + self._abort_if_unique_id_configured() + + try: + info = await validate_input(self.hass, user_input) + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/juicenet/const.py b/homeassistant/components/juicenet/const.py new file mode 100644 index 00000000000..5dc3e5c3e27 --- /dev/null +++ b/homeassistant/components/juicenet/const.py @@ -0,0 +1,6 @@ +"""Constants used by the JuiceNet component.""" + +DOMAIN = "juicenet" + +JUICENET_API = "juicenet_api" +JUICENET_COORDINATOR = "juicenet_coordinator" diff --git a/homeassistant/components/juicenet/device.py b/homeassistant/components/juicenet/device.py new file mode 100644 index 00000000000..86e1c92e4da --- /dev/null +++ b/homeassistant/components/juicenet/device.py @@ -0,0 +1,19 @@ +"""Adapter to wrap the pyjuicenet api for home assistant.""" + + +class JuiceNetApi: + """Represent a connection to JuiceNet.""" + + def __init__(self, api): + """Create an object from the provided API instance.""" + self.api = api + self._devices = [] + + async def setup(self): + """JuiceNet device setup.""" # noqa: D403 + self._devices = await self.api.get_devices() + + @property + def devices(self) -> list: + """Get a list of devices managed by this account.""" + return self._devices diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py new file mode 100644 index 00000000000..b3433948582 --- /dev/null +++ b/homeassistant/components/juicenet/entity.py @@ -0,0 +1,34 @@ +"""Adapter to wrap the pyjuicenet api for home assistant.""" + +from pyjuicenet import Charger + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +class JuiceNetDevice(CoordinatorEntity): + """Represent a base JuiceNet device.""" + + _attr_has_entity_name = True + + def __init__( + self, device: Charger, key: str, coordinator: DataUpdateCoordinator + ) -> None: + """Initialise the sensor.""" + super().__init__(coordinator) + self.device = device + self.key = key + self._attr_unique_id = f"{device.id}-{key}" + self._attr_device_info = DeviceInfo( + configuration_url=( + f"https://home.juice.net/Portal/Details?unitID={device.id}" + ), + identifiers={(DOMAIN, device.id)}, + manufacturer="JuiceNet", + name=device.name, + ) diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 5bdad83ac1e..979e540af01 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -1,9 +1,10 @@ { "domain": "juicenet", "name": "JuiceNet", - "codeowners": [], + "codeowners": ["@jesserockz"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/juicenet", - "integration_type": "system", "iot_class": "cloud_polling", - "requirements": [] + "loggers": ["pyjuicenet"], + "requirements": ["python-juicenet==1.1.0"] } diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py new file mode 100644 index 00000000000..fd2535c5bf3 --- /dev/null +++ b/homeassistant/components/juicenet/number.py @@ -0,0 +1,99 @@ +"""Support for controlling juicenet/juicepoint/juicebox based EVSE numbers.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyjuicenet import Api, Charger + +from homeassistant.components.number import ( + DEFAULT_MAX_VALUE, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice + + +@dataclass(frozen=True) +class JuiceNetNumberEntityDescriptionMixin: + """Mixin for required keys.""" + + setter_key: str + + +@dataclass(frozen=True) +class JuiceNetNumberEntityDescription( + NumberEntityDescription, JuiceNetNumberEntityDescriptionMixin +): + """An entity description for a JuiceNetNumber.""" + + native_max_value_key: str | None = None + + +NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = ( + JuiceNetNumberEntityDescription( + translation_key="amperage_limit", + key="current_charging_amperage_limit", + native_min_value=6, + native_max_value_key="max_charging_amperage", + native_step=1, + setter_key="set_charging_amperage_limit", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JuiceNet Numbers.""" + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api: Api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] + + entities = [ + JuiceNetNumber(device, description, coordinator) + for device in api.devices + for description in NUMBER_TYPES + ] + async_add_entities(entities) + + +class JuiceNetNumber(JuiceNetDevice, NumberEntity): + """Implementation of a JuiceNet number.""" + + entity_description: JuiceNetNumberEntityDescription + + def __init__( + self, + device: Charger, + description: JuiceNetNumberEntityDescription, + coordinator: DataUpdateCoordinator, + ) -> None: + """Initialise the number.""" + super().__init__(device, description.key, coordinator) + self.entity_description = description + + @property + def native_value(self) -> float | None: + """Return the value of the entity.""" + return getattr(self.device, self.entity_description.key, None) + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + if self.entity_description.native_max_value_key is not None: + return getattr(self.device, self.entity_description.native_max_value_key) + if self.entity_description.native_max_value is not None: + return self.entity_description.native_max_value + return DEFAULT_MAX_VALUE + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await getattr(self.device, self.entity_description.setter_key)(value) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py new file mode 100644 index 00000000000..5f71e066b9c --- /dev/null +++ b/homeassistant/components/juicenet/sensor.py @@ -0,0 +1,116 @@ +"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="status", + name="Charging Status", + ), + SensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + ), + SensorEntityDescription( + key="amps", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="watts", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="charge_time", + translation_key="charge_time", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:timer-outline", + ), + SensorEntityDescription( + key="energy_added", + translation_key="energy_added", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JuiceNet Sensors.""" + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] + + entities = [ + JuiceNetSensorDevice(device, coordinator, description) + for device in api.devices + for description in SENSOR_TYPES + ] + async_add_entities(entities) + + +class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): + """Implementation of a JuiceNet sensor.""" + + def __init__( + self, device, coordinator, description: SensorEntityDescription + ) -> None: + """Initialise the sensor.""" + super().__init__(device, description.key, coordinator) + self.entity_description = description + + @property + def icon(self): + """Return the icon of the sensor.""" + icon = None + if self.entity_description.key == "status": + status = self.device.status + if status == "standby": + icon = "mdi:power-plug-off" + elif status == "plugged": + icon = "mdi:power-plug" + elif status == "charging": + icon = "mdi:battery-positive" + else: + icon = self.entity_description.icon + return icon + + @property + def native_value(self): + """Return the state.""" + return getattr(self.device, self.entity_description.key, None) diff --git a/homeassistant/components/juicenet/strings.json b/homeassistant/components/juicenet/strings.json index 6e25130955b..0e3732c66d2 100644 --- a/homeassistant/components/juicenet/strings.json +++ b/homeassistant/components/juicenet/strings.json @@ -1,8 +1,41 @@ { - "issues": { - "integration_removed": { - "title": "The JuiceNet integration has been removed", - "description": "Enel X has dropped support for JuiceNet in favor of JuicePass, and the JuiceNet integration has been removed from Home Assistant as it was no longer working.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing JuiceNet integration entries]({entries})." + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "api_token": "[%key:common::config_flow::data::api_token%]" + }, + "description": "You will need the API Token from https://home.juice.net/Manage.", + "title": "Connect to JuiceNet" + } + } + }, + "entity": { + "number": { + "amperage_limit": { + "name": "Amperage limit" + } + }, + "sensor": { + "charge_time": { + "name": "Charge time" + }, + "energy_added": { + "name": "Energy added" + } + }, + "switch": { + "charge_now": { + "name": "Charge now" + } } } } diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py new file mode 100644 index 00000000000..7c373eeeb24 --- /dev/null +++ b/homeassistant/components/juicenet/switch.py @@ -0,0 +1,49 @@ +"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JuiceNet switches.""" + entities = [] + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] + + for device in api.devices: + entities.append(JuiceNetChargeNowSwitch(device, coordinator)) + async_add_entities(entities) + + +class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): + """Implementation of a JuiceNet switch.""" + + _attr_translation_key = "charge_now" + + def __init__(self, device, coordinator): + """Initialise the switch.""" + super().__init__(device, "charge_now", coordinator) + + @property + def is_on(self): + """Return true if switch is on.""" + return self.device.override_time != 0 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Charge now.""" + await self.device.set_override(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Don't charge now.""" + await self.device.set_override(False) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b657433b20b..6fc9d4dee27 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -256,6 +256,7 @@ FLOWS = { "isy994", "izone", "jellyfin", + "juicenet", "justnimbus", "jvc_projector", "kaleidescape", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 9c7a2001787..99b400119c6 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2911,6 +2911,12 @@ "config_flow": false, "iot_class": "cloud_push" }, + "juicenet": { + "name": "JuiceNet", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "justnimbus": { "name": "JustNimbus", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index fd453d2b93e..1105c612e1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2241,6 +2241,9 @@ python-izone==1.2.9 # homeassistant.components.joaoapps_join python-join-api==0.0.9 +# homeassistant.components.juicenet +python-juicenet==1.1.0 + # homeassistant.components.tplink python-kasa[speedups]==0.6.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6442ed4a72..6fec7801dd6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1723,6 +1723,9 @@ python-homewizard-energy==4.3.1 # homeassistant.components.izone python-izone==1.2.9 +# homeassistant.components.juicenet +python-juicenet==1.1.0 + # homeassistant.components.tplink python-kasa[speedups]==0.6.2.1 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 9ad9c3676b8..8b9f73336fe 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -33,6 +33,7 @@ IGNORE_VIOLATIONS = { "blink", "ezviz", "hdmi_cec", + "juicenet", "lupusec", "rainbird", "slide", diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py new file mode 100644 index 00000000000..6adc841862e --- /dev/null +++ b/tests/components/juicenet/test_config_flow.py @@ -0,0 +1,124 @@ +"""Test the JuiceNet config flow.""" +from unittest.mock import MagicMock, patch + +import aiohttp +from pyjuicenet import TokenError + +from homeassistant import config_entries +from homeassistant.components.juicenet.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + + +def _mock_juicenet_return_value(get_devices=None): + juicenet_mock = MagicMock() + type(juicenet_mock).get_devices = MagicMock(return_value=get_devices) + return juicenet_mock + + +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"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.juicenet.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "JuiceNet" + assert result2["data"] == {CONF_ACCESS_TOKEN: "access_token"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +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( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + side_effect=TokenError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> 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.juicenet.config_flow.Api.get_devices", + side_effect=aiohttp.ClientError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_catch_unknown_errors(hass: HomeAssistant) -> 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.juicenet.config_flow.Api.get_devices", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_import(hass: HomeAssistant) -> None: + """Test that import works as expected.""" + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.juicenet.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_ACCESS_TOKEN: "access_token"}, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "JuiceNet" + assert result["data"] == {CONF_ACCESS_TOKEN: "access_token"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/juicenet/test_init.py b/tests/components/juicenet/test_init.py deleted file mode 100644 index 8896798abe3..00000000000 --- a/tests/components/juicenet/test_init.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Tests for the JuiceNet component.""" - -from homeassistant.components.juicenet import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from tests.common import MockConfigEntry - - -async def test_juicenet_repair_issue( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test the JuiceNet configuration entry loading/unloading handles the repair.""" - config_entry_1 = MockConfigEntry( - title="Example 1", - domain=DOMAIN, - ) - config_entry_1.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_1.entry_id) - await hass.async_block_till_done() - assert config_entry_1.state is ConfigEntryState.LOADED - - # Add a second one - config_entry_2 = MockConfigEntry( - title="Example 2", - domain=DOMAIN, - ) - config_entry_2.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_2.entry_id) - await hass.async_block_till_done() - - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - - # Remove the first one - await hass.config_entries.async_remove(config_entry_1.entry_id) - await hass.async_block_till_done() - - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - - # Remove the second one - await hass.config_entries.async_remove(config_entry_2.entry_id) - await hass.async_block_till_done() - - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.NOT_LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None