Add Uonet+ Vulcan integration (#56357)

* Add Uonet+ Vulcan integration

* Add "configuration_url", fix some minor issues

* Refactor and fix tests

* Optimize code

* Apply suggestion from code review

Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>

* Update error handling

* Fix some tests

* Update CODEOWNERS and fix pylint

* Cleanup vulcan tests

* Run prettier

Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Antoni Czaplicki 2022-03-30 17:55:24 +02:00 committed by GitHub
parent 4d607b2eb5
commit 76f07ec240
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1921 additions and 0 deletions

View File

@ -1379,6 +1379,9 @@ omit =
homeassistant/components/volumio/browse_media.py
homeassistant/components/volumio/media_player.py
homeassistant/components/volvooncall/*
homeassistant/components/vulcan/__init__.py
homeassistant/components/vulcan/calendar.py
homeassistant/components/vulcan/fetch_data.py
homeassistant/components/w800rf32/*
homeassistant/components/waqi/sensor.py
homeassistant/components/waterfurnace/*

View File

@ -1121,6 +1121,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/volumio/ @OnFreund
/tests/components/volumio/ @OnFreund
/homeassistant/components/volvooncall/ @molobrakos @decompil3d
/homeassistant/components/vulcan/ @Antoni-Czaplicki
/tests/components/vulcan/ @Antoni-Czaplicki
/homeassistant/components/wake_on_lan/ @ntilley905
/tests/components/wake_on_lan/ @ntilley905
/homeassistant/components/wallbox/ @hesselonline

View File

@ -0,0 +1,75 @@
"""The Vulcan component."""
import logging
from aiohttp import ClientConnectorError
from vulcan import Account, Keystore, Vulcan
from vulcan._utils import VulcanAPIException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["calendar"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Uonet+ Vulcan integration."""
hass.data.setdefault(DOMAIN, {})
try:
keystore = Keystore.load(entry.data["keystore"])
account = Account.load(entry.data["account"])
client = Vulcan(keystore, account)
await client.select_student()
students = await client.get_students()
for student in students:
if str(student.pupil.id) == str(entry.data["student_id"]):
client.student = student
break
except VulcanAPIException as err:
if str(err) == "The certificate is not authorized.":
_LOGGER.error(
"The certificate is not authorized, please authorize integration again"
)
raise ConfigEntryAuthFailed from err
_LOGGER.error("Vulcan API error: %s", err)
return False
except ClientConnectorError as err:
if "connection_error" not in hass.data[DOMAIN]:
_LOGGER.error(
"Connection error - please check your internet connection: %s", err
)
hass.data[DOMAIN]["connection_error"] = True
await client.close()
raise ConfigEntryNotReady from err
hass.data[DOMAIN]["students_number"] = len(
hass.config_entries.async_entries(DOMAIN)
)
hass.data[DOMAIN][entry.entry_id] = client
if not entry.update_listeners:
entry.add_update_listener(_async_update_options)
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)
return True
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
await hass.data[DOMAIN][entry.entry_id].close()
for platform in PLATFORMS:
await hass.config_entries.async_forward_entry_unload(entry, platform)
return True
async def _async_update_options(hass, entry):
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -0,0 +1,219 @@
"""Support for Vulcan Calendar platform."""
import copy
from datetime import date, datetime, timedelta
import logging
from aiohttp import ClientConnectorError
from vulcan._utils import VulcanAPIException
from homeassistant.components.calendar import ENTITY_ID_FORMAT, CalendarEventDevice
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.util import Throttle, dt
from . import DOMAIN
from .const import DEFAULT_SCAN_INTERVAL
from .fetch_data import get_lessons, get_student_info
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the calendar platform for event devices."""
VulcanCalendarData.MIN_TIME_BETWEEN_UPDATES = timedelta(
minutes=config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
)
client = hass.data[DOMAIN][config_entry.entry_id]
data = {
"student_info": await get_student_info(
client, config_entry.data.get("student_id")
),
"students_number": hass.data[DOMAIN]["students_number"],
}
async_add_entities(
[
VulcanCalendarEventDevice(
client,
data,
generate_entity_id(
ENTITY_ID_FORMAT,
f"vulcan_calendar_{data['student_info']['full_name']}",
hass=hass,
),
)
],
)
class VulcanCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
def __init__(self, client, data, entity_id):
"""Create the Calendar event device."""
self.student_info = data["student_info"]
self.data = VulcanCalendarData(
client,
self.student_info,
self.hass,
)
self._event = None
self.entity_id = entity_id
self._unique_id = f"vulcan_calendar_{self.student_info['id']}"
if data["students_number"] == 1:
self._attr_name = "Vulcan calendar"
self.device_name = "Calendar"
else:
self._attr_name = f"Vulcan calendar - {self.student_info['full_name']}"
self.device_name = f"{self.student_info['full_name']}: Calendar"
self._attr_unique_id = f"vulcan_calendar_{self.student_info['id']}"
self._attr_device_info = {
"identifiers": {(DOMAIN, f"calendar_{self.student_info['id']}")},
"entry_type": DeviceEntryType.SERVICE,
"name": self.device_name,
"model": f"{self.student_info['full_name']} - {self.student_info['class']} {self.student_info['school']}",
"manufacturer": "Uonet +",
"configuration_url": f"https://uonetplus.vulcan.net.pl/{self.student_info['symbol']}",
}
@property
def event(self):
"""Return the next upcoming event."""
return self._event
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
return await self.data.async_get_events(hass, start_date, end_date)
async def async_update(self):
"""Update event data."""
await self.data.async_update()
event = copy.deepcopy(self.data.event)
if event is None:
self._event = event
return
event["start"] = {
"dateTime": datetime.combine(event["date"], event["time"].from_)
.astimezone(dt.DEFAULT_TIME_ZONE)
.isoformat()
}
event["end"] = {
"dateTime": datetime.combine(event["date"], event["time"].to)
.astimezone(dt.DEFAULT_TIME_ZONE)
.isoformat()
}
self._event = event
class VulcanCalendarData:
"""Class to utilize calendar service object to get next event."""
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=DEFAULT_SCAN_INTERVAL)
def __init__(self, client, student_info, hass):
"""Set up how we are going to search the Vulcan calendar."""
self.client = client
self.event = None
self.hass = hass
self.student_info = student_info
self._available = True
async def async_get_events(self, hass, start_date, end_date):
"""Get all events in a specific time frame."""
try:
events = await get_lessons(
self.client,
date_from=start_date,
date_to=end_date,
)
except VulcanAPIException as err:
if str(err) == "The certificate is not authorized.":
_LOGGER.error(
"The certificate is not authorized, please authorize integration again"
)
raise ConfigEntryAuthFailed from err
_LOGGER.error("An API error has occurred: %s", err)
events = []
except ClientConnectorError as err:
if self._available:
_LOGGER.warning(
"Connection error - please check your internet connection: %s", err
)
events = []
event_list = []
for item in events:
event = {
"uid": item["id"],
"start": {
"dateTime": datetime.combine(
item["date"], item["time"].from_
).strftime(DATE_STR_FORMAT)
},
"end": {
"dateTime": datetime.combine(
item["date"], item["time"].to
).strftime(DATE_STR_FORMAT)
},
"summary": item["lesson"],
"location": item["room"],
"description": item["teacher"],
}
event_list.append(event)
return event_list
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Get the latest data."""
try:
events = await get_lessons(self.client)
if not self._available:
_LOGGER.info("Restored connection with API")
self._available = True
if events == []:
events = await get_lessons(
self.client,
date_to=date.today() + timedelta(days=7),
)
if events == []:
self.event = None
return
except VulcanAPIException as err:
if str(err) == "The certificate is not authorized.":
_LOGGER.error(
"The certificate is not authorized, please authorize integration again"
)
raise ConfigEntryAuthFailed from err
_LOGGER.error("An API error has occurred: %s", err)
return
except ClientConnectorError as err:
if self._available:
_LOGGER.warning(
"Connection error - please check your internet connection: %s", err
)
self._available = False
return
new_event = min(
events,
key=lambda d: (
datetime.combine(d["date"], d["time"].to) < datetime.now(),
abs(datetime.combine(d["date"], d["time"].to) - datetime.now()),
),
)
self.event = {
"uid": new_event["id"],
"date": new_event["date"],
"time": new_event["time"],
"summary": new_event["lesson"],
"location": new_event["room"],
"description": new_event["teacher"],
}

View File

@ -0,0 +1,342 @@
"""Adds config flow for Vulcan."""
import logging
from aiohttp import ClientConnectionError
import voluptuous as vol
from vulcan import Account, Keystore, Vulcan
from vulcan._utils import VulcanAPIException
from homeassistant import config_entries
from homeassistant.const import CONF_PIN, CONF_REGION, CONF_SCAN_INTERVAL, CONF_TOKEN
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from . import DOMAIN
from .const import DEFAULT_SCAN_INTERVAL
from .register import register
_LOGGER = logging.getLogger(__name__)
LOGIN_SCHEMA = {
vol.Required(CONF_TOKEN): str,
vol.Required(CONF_REGION): str,
vol.Required(CONF_PIN): str,
}
class VulcanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a Uonet+ Vulcan config flow."""
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return VulcanOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None):
"""Handle config flow."""
if self._async_current_entries():
return await self.async_step_add_next_config_entry()
return await self.async_step_auth()
async def async_step_auth(self, user_input=None, errors=None):
"""Authorize integration."""
if user_input is not None:
try:
credentials = await register(
self.hass,
user_input[CONF_TOKEN],
user_input[CONF_REGION],
user_input[CONF_PIN],
)
except VulcanAPIException as err:
if str(err) == "Invalid token!" or str(err) == "Invalid token.":
errors = {"base": "invalid_token"}
elif str(err) == "Expired token.":
errors = {"base": "expired_token"}
elif str(err) == "Invalid PIN.":
errors = {"base": "invalid_pin"}
else:
errors = {"base": "unknown"}
_LOGGER.error(err)
except RuntimeError as err:
if str(err) == "Internal Server Error (ArgumentException)":
errors = {"base": "invalid_symbol"}
else:
errors = {"base": "unknown"}
_LOGGER.error(err)
except ClientConnectionError as err:
errors = {"base": "cannot_connect"}
_LOGGER.error("Connection error: %s", err)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors = {"base": "unknown"}
if not errors:
account = credentials["account"]
keystore = credentials["keystore"]
client = Vulcan(keystore, account)
students = await client.get_students()
await client.close()
if len(students) > 1:
# pylint:disable=attribute-defined-outside-init
self.account = account
self.keystore = keystore
self.students = students
return await self.async_step_select_student()
student = students[0]
await self.async_set_unique_id(str(student.pupil.id))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{student.pupil.first_name} {student.pupil.last_name}",
data={
"student_id": str(student.pupil.id),
"keystore": keystore.as_dict,
"account": account.as_dict,
},
)
return self.async_show_form(
step_id="auth",
data_schema=vol.Schema(LOGIN_SCHEMA),
errors=errors,
)
async def async_step_select_student(self, user_input=None):
"""Allow user to select student."""
errors = {}
students_list = {}
if self.students is not None:
for student in self.students:
students_list[
str(student.pupil.id)
] = f"{student.pupil.first_name} {student.pupil.last_name}"
if user_input is not None:
student_id = user_input["student"]
await self.async_set_unique_id(str(student_id))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=students_list[student_id],
data={
"student_id": str(student_id),
"keystore": self.keystore.as_dict,
"account": self.account.as_dict,
},
)
data_schema = {
vol.Required(
"student",
): vol.In(students_list),
}
return self.async_show_form(
step_id="select_student",
data_schema=vol.Schema(data_schema),
errors=errors,
)
async def async_step_select_saved_credentials(self, user_input=None, errors=None):
"""Allow user to select saved credentials."""
credentials_list = {}
for entry in self.hass.config_entries.async_entries(DOMAIN):
credentials_list[entry.entry_id] = entry.data["account"]["UserName"]
if user_input is not None:
entry = self.hass.config_entries.async_get_entry(user_input["credentials"])
keystore = Keystore.load(entry.data["keystore"])
account = Account.load(entry.data["account"])
client = Vulcan(keystore, account)
try:
students = await client.get_students()
except VulcanAPIException as err:
if str(err) == "The certificate is not authorized.":
return await self.async_step_auth(
errors={"base": "expired_credentials"}
)
_LOGGER.error(err)
return await self.async_step_auth(errors={"base": "unknown"})
except ClientConnectionError as err:
_LOGGER.error("Connection error: %s", err)
return await self.async_step_select_saved_credentials(
errors={"base": "cannot_connect"}
)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
return await self.async_step_auth(errors={"base": "unknown"})
finally:
await client.close()
if len(students) == 1:
student = students[0]
await self.async_set_unique_id(str(student.pupil.id))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{student.pupil.first_name} {student.pupil.last_name}",
data={
"student_id": str(student.pupil.id),
"keystore": keystore.as_dict,
"account": account.as_dict,
},
)
# pylint:disable=attribute-defined-outside-init
self.account = account
self.keystore = keystore
self.students = students
return await self.async_step_select_student()
data_schema = {
vol.Required(
"credentials",
): vol.In(credentials_list),
}
return self.async_show_form(
step_id="select_saved_credentials",
data_schema=vol.Schema(data_schema),
errors=errors,
)
async def async_step_add_next_config_entry(self, user_input=None):
"""Flow initialized when user is adding next entry of that integration."""
existing_entries = []
for entry in self.hass.config_entries.async_entries(DOMAIN):
existing_entries.append(entry)
errors = {}
if user_input is not None:
if user_input["use_saved_credentials"]:
if len(existing_entries) == 1:
keystore = Keystore.load(existing_entries[0].data["keystore"])
account = Account.load(existing_entries[0].data["account"])
client = Vulcan(keystore, account)
students = await client.get_students()
await client.close()
new_students = []
existing_entry_ids = []
for entry in self.hass.config_entries.async_entries(DOMAIN):
existing_entry_ids.append(entry.data["student_id"])
for student in students:
if str(student.pupil.id) not in existing_entry_ids:
new_students.append(student)
if not new_students:
return self.async_abort(reason="all_student_already_configured")
if len(new_students) == 1:
await self.async_set_unique_id(str(new_students[0].pupil.id))
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=f"{new_students[0].pupil.first_name} {new_students[0].pupil.last_name}",
data={
"student_id": str(new_students[0].pupil.id),
"keystore": keystore.as_dict,
"account": account.as_dict,
},
)
# pylint:disable=attribute-defined-outside-init
self.account = account
self.keystore = keystore
self.students = new_students
return await self.async_step_select_student()
return await self.async_step_select_saved_credentials()
return await self.async_step_auth()
data_schema = {
vol.Required("use_saved_credentials", default=True): bool,
}
return self.async_show_form(
step_id="add_next_config_entry",
data_schema=vol.Schema(data_schema),
errors=errors,
)
async def async_step_reauth(self, user_input=None):
"""Reauthorize integration."""
errors = {}
if user_input is not None:
try:
credentials = await register(
self.hass,
user_input[CONF_TOKEN],
user_input[CONF_REGION],
user_input[CONF_PIN],
)
except VulcanAPIException as err:
if str(err) == "Invalid token!" or str(err) == "Invalid token.":
errors["base"] = "invalid_token"
elif str(err) == "Expired token.":
errors["base"] = "expired_token"
elif str(err) == "Invalid PIN.":
errors["base"] = "invalid_pin"
else:
errors["base"] = "unknown"
_LOGGER.error(err)
except RuntimeError as err:
if str(err) == "Internal Server Error (ArgumentException)":
errors["base"] = "invalid_symbol"
else:
errors["base"] = "unknown"
_LOGGER.error(err)
except ClientConnectionError as err:
errors["base"] = "cannot_connect"
_LOGGER.error("Connection error: %s", err)
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
account = credentials["account"]
keystore = credentials["keystore"]
client = Vulcan(keystore, account)
students = await client.get_students()
await client.close()
existing_entries = []
for entry in self.hass.config_entries.async_entries(DOMAIN):
existing_entries.append(entry)
for student in students:
for entry in existing_entries:
if str(student.pupil.id) == str(entry.data["student_id"]):
self.hass.config_entries.async_update_entry(
entry,
title=f"{student.pupil.first_name} {student.pupil.last_name}",
data={
"student_id": str(student.pupil.id),
"keystore": keystore.as_dict,
"account": account.as_dict,
},
)
await self.hass.config_entries.async_reload(entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth",
data_schema=vol.Schema(LOGIN_SCHEMA),
errors=errors,
)
class VulcanOptionsFlowHandler(config_entries.OptionsFlow):
"""Config flow options for Uonet+ Vulcan."""
def __init__(self, config_entry):
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
"""Manage the options."""
errors = {}
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
options = {
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): cv.positive_int,
}
return self.async_show_form(
step_id="init", data_schema=vol.Schema(options), errors=errors
)

View File

@ -0,0 +1,4 @@
"""Constants for the Vulcan integration."""
DOMAIN = "vulcan"
DEFAULT_SCAN_INTERVAL = 5

View File

@ -0,0 +1,97 @@
"""Support for fetching Vulcan data."""
async def get_lessons(client, date_from=None, date_to=None):
"""Support for fetching Vulcan lessons."""
changes = {}
list_ans = []
async for lesson in await client.data.get_changed_lessons(
date_from=date_from, date_to=date_to
):
temp_dict = {}
_id = str(lesson.id)
temp_dict["id"] = lesson.id
temp_dict["number"] = lesson.time.position if lesson.time is not None else None
temp_dict["lesson"] = (
lesson.subject.name if lesson.subject is not None else None
)
temp_dict["room"] = lesson.room.code if lesson.room is not None else None
temp_dict["changes"] = lesson.changes
temp_dict["note"] = lesson.note
temp_dict["reason"] = lesson.reason
temp_dict["event"] = lesson.event
temp_dict["group"] = lesson.group
temp_dict["teacher"] = (
lesson.teacher.display_name if lesson.teacher is not None else None
)
temp_dict["from_to"] = (
lesson.time.displayed_time if lesson.time is not None else None
)
changes[str(_id)] = temp_dict
async for lesson in await client.data.get_lessons(
date_from=date_from, date_to=date_to
):
temp_dict = {}
temp_dict["id"] = lesson.id
temp_dict["number"] = lesson.time.position
temp_dict["time"] = lesson.time
temp_dict["date"] = lesson.date.date
temp_dict["lesson"] = (
lesson.subject.name if lesson.subject is not None else None
)
if lesson.room is not None:
temp_dict["room"] = lesson.room.code
else:
temp_dict["room"] = "-"
temp_dict["visible"] = lesson.visible
temp_dict["changes"] = lesson.changes
temp_dict["group"] = lesson.group
temp_dict["reason"] = None
temp_dict["teacher"] = (
lesson.teacher.display_name if lesson.teacher is not None else None
)
temp_dict["from_to"] = (
lesson.time.displayed_time if lesson.time is not None else None
)
if temp_dict["changes"] is None:
temp_dict["changes"] = ""
elif temp_dict["changes"].type == 1:
temp_dict["lesson"] = f"Lekcja odwołana ({temp_dict['lesson']})"
temp_dict["changes_info"] = f"Lekcja odwołana ({temp_dict['lesson']})"
if str(temp_dict["changes"].id) in changes:
temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"]
elif temp_dict["changes"].type == 2:
temp_dict["lesson"] = f"{temp_dict['lesson']} (Zastępstwo)"
temp_dict["teacher"] = changes[str(temp_dict["changes"].id)]["teacher"]
if str(temp_dict["changes"].id) in changes:
temp_dict["teacher"] = changes[str(temp_dict["changes"].id)]["teacher"]
temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"]
elif temp_dict["changes"].type == 4:
temp_dict["lesson"] = f"Lekcja odwołana ({temp_dict['lesson']})"
if str(temp_dict["changes"].id) in changes:
temp_dict["reason"] = changes[str(temp_dict["changes"].id)]["reason"]
if temp_dict["visible"]:
list_ans.append(temp_dict)
return list_ans
async def get_student_info(client, student_id):
"""Support for fetching Student info by student id."""
student_info = {}
for student in await client.get_students():
if str(student.pupil.id) == str(student_id):
student_info["first_name"] = student.pupil.first_name
if student.pupil.second_name:
student_info["second_name"] = student.pupil.second_name
student_info["last_name"] = student.pupil.last_name
student_info[
"full_name"
] = f"{student.pupil.first_name} {student.pupil.last_name}"
student_info["id"] = student.pupil.id
student_info["class"] = student.class_
student_info["school"] = student.school.name
student_info["symbol"] = student.symbol
return student_info

View File

@ -0,0 +1,11 @@
{
"domain": "vulcan",
"name": "Uonet+ Vulcan",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/vulcan",
"requirements": ["vulcan-api==2.0.3"],
"dependencies": [],
"codeowners": ["@Antoni-Czaplicki"],
"iot_class": "cloud_polling",
"quality_scale": "platinum"
}

View File

@ -0,0 +1,13 @@
"""Support for register Vulcan account."""
from functools import partial
from vulcan import Account, Keystore
async def register(hass, token, symbol, pin):
"""Register integration and save credentials."""
keystore = await hass.async_add_executor_job(
partial(Keystore.create, device_model="Home Assistant")
)
account = await Account.register(keystore, token, symbol, pin)
return {"account": account, "keystore": keystore}

View File

@ -0,0 +1,69 @@
{
"config": {
"abort": {
"already_configured": "That student has already been added.",
"all_student_already_configured": "All students have already been added.",
"reauth_successful": "Reauth successful"
},
"error": {
"unknown": "Unknown error occurred",
"invalid_token": "Invalid token",
"expired_token": "Expired token - please generate a new token",
"invalid_pin": "Invalid pin",
"invalid_symbol": "Invalid symbol",
"expired_credentials": "Expired credentials - please create new on Vulcan mobile app registration page",
"cannot_connect": "Connection error - please check your internet connection"
},
"step": {
"auth": {
"description": "Login to your Vulcan Account using mobile app registration page.",
"data": {
"token": "Token",
"region": "Symbol",
"pin": "Pin"
}
},
"reauth": {
"description": "Login to your Vulcan Account using mobile app registration page.",
"data": {
"token": "Token",
"region": "Symbol",
"pin": "Pin"
}
},
"select_student": {
"description": "Select student, you can add more students by adding integration again.",
"data": {
"student_name": "Select student"
}
},
"select_saved_credentials": {
"description": "Select saved credentials.",
"data": {
"credentials": "Login"
}
},
"add_next_config_entry": {
"description": "Add another student.",
"data": {
"use_saved_credentials": "Use saved credentials"
}
}
}
},
"options": {
"error": {
"error": "Error occurred"
},
"step": {
"init": {
"data": {
"message_notify": "Show notifications when new message received",
"attendance_notify": "Show notifications about the latest attendance entries",
"grade_notify": "Show notifications about the latest grades",
"scan_interval": "Update interval (in minutes)"
}
}
}
}
}

View File

@ -0,0 +1,69 @@
{
"config": {
"abort": {
"already_configured": "That student has already been added.",
"all_student_already_configured": "All students have already been added.",
"reauth_successful": "Reauth successful"
},
"error": {
"unknown": "Unknown error occurred",
"invalid_token": "Invalid token",
"expired_token": "Expired token - please generate a new token",
"invalid_pin": "Invalid pin",
"invalid_symbol": "Invalid symbol",
"expired_credentials": "Expired credentials - please create new on Vulcan mobile app registration page",
"cannot_connect": "Connection error - please check your internet connection"
},
"step": {
"auth": {
"description": "Login to your Vulcan Account using mobile app registration page.",
"data": {
"token": "Token",
"region": "Symbol",
"pin": "Pin"
}
},
"reauth": {
"description": "Login to your Vulcan Account using mobile app registration page.",
"data": {
"token": "Token",
"region": "Symbol",
"pin": "Pin"
}
},
"select_student": {
"description": "Select student, you can add more students by adding integration again.",
"data": {
"student_name": "Select student"
}
},
"select_saved_credentials": {
"description": "Select saved credentials.",
"data": {
"credentials": "Login"
}
},
"add_next_config_entry": {
"description": "Add another student.",
"data": {
"use_saved_credentials": "Use saved credentials"
}
}
}
},
"options": {
"error": {
"error": "Error occurred"
},
"step": {
"init": {
"data": {
"message_notify": "Show notifications when new message received",
"attendance_notify": "Show notifications about the latest attendance entries",
"grade_notify": "Show notifications about the latest grades",
"scan_interval": "Update interval (in minutes)"
}
}
}
}
}

View File

@ -376,6 +376,7 @@ FLOWS = {
"vizio",
"vlc_telnet",
"volumio",
"vulcan",
"wallbox",
"watttime",
"waze_travel_time",

View File

@ -2378,6 +2378,9 @@ vsure==1.7.3
# homeassistant.components.vasttrafik
vtjp==0.1.14
# homeassistant.components.vulcan
vulcan-api==2.0.3
# homeassistant.components.vultr
vultr==0.1.2

View File

@ -1531,6 +1531,9 @@ vilfo-api-client==0.3.2
# homeassistant.components.verisure
vsure==1.7.3
# homeassistant.components.vulcan
vulcan-api==2.0.3
# homeassistant.components.vultr
vultr==0.1.2

View File

@ -0,0 +1 @@
"""Tests for the Uonet+ Vulcan integration."""

View File

@ -0,0 +1,16 @@
{
"student_id": "123",
"keystore": {
"Certificate": "certificate",
"DeviceModel": "Home Assistant",
"Fingerprint": "fingerprint",
"FirebaseToken": "firebase_token",
"PrivateKey": "private_key"
},
"account": {
"LoginId": 0,
"RestURL": "",
"UserLogin": "example@example.com",
"UserName": "example@example.com"
}
}

View File

@ -0,0 +1,29 @@
{
"TopLevelPartition": "",
"Partition": "",
"ClassDisplay": "",
"Unit": {
"Id": 1,
"Symbol": "",
"Short": "",
"RestURL": "",
"Name": "",
"DisplayName": ""
},
"ConstituentUnit": {
"Id": 1,
"Short": "",
"Name": "",
"Address": ""
},
"Pupil": {
"Id": 0,
"LoginId": 0,
"LoginValue": "",
"FirstName": "Jan",
"SecondName": "Maciej",
"Surname": "Kowalski",
"Sex": true
},
"Periods": []
}

View File

@ -0,0 +1,29 @@
{
"TopLevelPartition": "",
"Partition": "",
"ClassDisplay": "",
"Unit": {
"Id": 1,
"Symbol": "",
"Short": "",
"RestURL": "",
"Name": "",
"DisplayName": ""
},
"ConstituentUnit": {
"Id": 1,
"Short": "",
"Name": "",
"Address": ""
},
"Pupil": {
"Id": 1,
"LoginId": 1,
"LoginValue": "",
"FirstName": "Magda",
"SecondName": "",
"Surname": "Kowalska",
"Sex": false
},
"Periods": []
}

View File

@ -0,0 +1,935 @@
"""Test the Uonet+ Vulcan config flow."""
import json
from unittest import mock
from unittest.mock import patch
from vulcan import Account
from vulcan.model import Student
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.vulcan import config_flow, const, register
from homeassistant.components.vulcan.config_flow import (
ClientConnectionError,
Keystore,
VulcanAPIException,
)
from homeassistant.const import CONF_PIN, CONF_REGION, CONF_SCAN_INTERVAL, CONF_TOKEN
from tests.common import MockConfigEntry, load_fixture
fake_keystore = Keystore("", "", "", "", "")
fake_account = Account(
login_id=1,
user_login="example@example.com",
user_name="example@example.com",
rest_url="rest_url",
)
async def test_show_form(hass):
"""Test that the form is served with no input."""
flow = config_flow.VulcanFlowHandler()
flow.hass = hass
result = await flow.async_step_user(user_input=None)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
@mock.patch("homeassistant.components.vulcan.config_flow.Account.register")
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_success(
mock_keystore, mock_account, mock_student, hass
):
"""Test a successful config flow initialized by the user."""
mock_keystore.return_value = fake_keystore
mock_account.return_value = fake_account
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
]
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Jan Kowalski"
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
@mock.patch("homeassistant.components.vulcan.config_flow.Account.register")
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_success_with_multiple_students(
mock_keystore, mock_account, mock_student, hass
):
"""Test a successful config flow with multiple students."""
mock_keystore.return_value = fake_keystore
mock_account.return_value = fake_account
mock_student.return_value = [
Student.load(student)
for student in [load_fixture("fake_student_1.json", "vulcan")]
+ [load_fixture("fake_student_2.json", "vulcan")]
]
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_student"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"student": "0"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Jan Kowalski"
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
@mock.patch("homeassistant.components.vulcan.config_flow.Account.register")
async def test_config_flow_reauth_success(
mock_account, mock_keystore, mock_student, hass
):
"""Test a successful config flow reauth."""
mock_keystore.return_value = fake_keystore
mock_account.return_value = fake_account
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
]
MockConfigEntry(
domain=const.DOMAIN,
unique_id="0",
data={"student_id": "0", "login": "example@example.com"},
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "reauth_successful"
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
@mock.patch("homeassistant.components.vulcan.config_flow.Account.register")
async def test_config_flow_reauth_with_errors(mock_account, mock_keystore, hass):
"""Test reauth config flow with errors."""
mock_keystore.return_value = fake_keystore
mock_account.return_value = fake_account
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_REAUTH}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Invalid token."),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "invalid_token"}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Expired token."),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "expired_token"}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Invalid PIN."),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "invalid_pin"}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Unknown error"),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "unknown"}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=RuntimeError("Internal Server Error (ArgumentException)"),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "invalid_symbol"}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=RuntimeError("Unknown error"),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "unknown"}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=ClientConnectionError,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "cannot_connect"}
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=Exception,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "reauth"
assert result["errors"] == {"base": "unknown"}
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
@mock.patch("homeassistant.components.vulcan.config_flow.Account.register")
async def test_multiple_config_entries(mock_account, mock_keystore, mock_student, hass):
"""Test a successful config flow for multiple config entries."""
mock_keystore.return_value = fake_keystore
mock_account.return_value = fake_account
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
]
MockConfigEntry(
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
await register.register(hass, "token", "region", "000000")
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": False},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "token", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Jan Kowalski"
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
async def test_multiple_config_entries_using_saved_credentials(mock_student, hass):
"""Test a successful config flow for multiple config entries using saved credentials."""
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
]
MockConfigEntry(
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Jan Kowalski"
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
async def test_multiple_config_entries_using_saved_credentials_2(mock_student, hass):
"""Test a successful config flow for multiple config entries using saved credentials (different situation)."""
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
] + [Student.load(load_fixture("fake_student_2.json", "vulcan"))]
MockConfigEntry(
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_student"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"student": "0"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Jan Kowalski"
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
async def test_multiple_config_entries_using_saved_credentials_3(mock_student, hass):
"""Test a successful config flow for multiple config entries using saved credentials."""
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
]
MockConfigEntry(
entry_id="456",
domain=const.DOMAIN,
unique_id="234567",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan"))
| {"student_id": "456"},
).add_to_hass(hass)
MockConfigEntry(
entry_id="123",
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_saved_credentials"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"credentials": "123"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Jan Kowalski"
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
async def test_multiple_config_entries_using_saved_credentials_4(mock_student, hass):
"""Test a successful config flow for multiple config entries using saved credentials (different situation)."""
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
] + [Student.load(load_fixture("fake_student_2.json", "vulcan"))]
MockConfigEntry(
entry_id="456",
domain=const.DOMAIN,
unique_id="234567",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan"))
| {"student_id": "456"},
).add_to_hass(hass)
MockConfigEntry(
entry_id="123",
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_saved_credentials"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"credentials": "123"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_student"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"student": "0"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert result["title"] == "Jan Kowalski"
async def test_multiple_config_entries_without_valid_saved_credentials(hass):
"""Test a unsuccessful config flow for multiple config entries without valid saved credentials."""
MockConfigEntry(
entry_id="456",
domain=const.DOMAIN,
unique_id="234567",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan"))
| {"student_id": "456"},
).add_to_hass(hass)
MockConfigEntry(
entry_id="123",
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
with patch(
"homeassistant.components.vulcan.config_flow.Vulcan.get_students",
side_effect=VulcanAPIException("The certificate is not authorized."),
):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_saved_credentials"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"credentials": "123"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "expired_credentials"}
async def test_multiple_config_entries_using_saved_credentials_with_connections_issues(
hass,
):
"""Test a unsuccessful config flow for multiple config entries without valid saved credentials."""
MockConfigEntry(
entry_id="456",
domain=const.DOMAIN,
unique_id="234567",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan"))
| {"student_id": "456"},
).add_to_hass(hass)
MockConfigEntry(
entry_id="123",
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
with patch(
"homeassistant.components.vulcan.config_flow.Vulcan.get_students",
side_effect=ClientConnectionError,
):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_saved_credentials"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"credentials": "123"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_saved_credentials"
assert result["errors"] == {"base": "cannot_connect"}
async def test_multiple_config_entries_using_saved_credentials_with_unknown_error(hass):
"""Test a unsuccessful config flow for multiple config entries without valid saved credentials."""
MockConfigEntry(
entry_id="456",
domain=const.DOMAIN,
unique_id="234567",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan"))
| {"student_id": "456"},
).add_to_hass(hass)
MockConfigEntry(
entry_id="123",
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
with patch(
"homeassistant.components.vulcan.config_flow.Vulcan.get_students",
side_effect=Exception,
):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_saved_credentials"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"credentials": "123"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "unknown"}
async def test_multiple_config_entries_using_saved_credentials_with_unknown_api_error(
hass,
):
"""Test a unsuccessful config flow for multiple config entries without valid saved credentials."""
MockConfigEntry(
entry_id="456",
domain=const.DOMAIN,
unique_id="234567",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan"))
| {"student_id": "456"},
).add_to_hass(hass)
MockConfigEntry(
entry_id="123",
domain=const.DOMAIN,
unique_id="123456",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
with patch(
"homeassistant.components.vulcan.config_flow.Vulcan.get_students",
side_effect=VulcanAPIException("Unknown error"),
):
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "select_saved_credentials"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"credentials": "123"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "unknown"}
@mock.patch("homeassistant.components.vulcan.config_flow.Vulcan.get_students")
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
@mock.patch("homeassistant.components.vulcan.config_flow.Account.register")
async def test_student_already_exists(mock_account, mock_keystore, mock_student, hass):
"""Test config entry when student's entry already exists."""
mock_keystore.return_value = fake_keystore
mock_account.return_value = fake_account
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
]
MockConfigEntry(
domain=const.DOMAIN,
unique_id="0",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan"))
| {"student_id": "0"},
).add_to_hass(hass)
await register.register(hass, "token", "region", "000000")
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "add_next_config_entry"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"use_saved_credentials": True},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "all_student_already_configured"
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_invalid_token(mock_keystore, hass):
"""Test a config flow initialized by the user using invalid token."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Invalid token."),
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S20000", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "invalid_token"}
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_invalid_region(mock_keystore, hass):
"""Test a config flow initialized by the user using invalid region."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=RuntimeError("Internal Server Error (ArgumentException)"),
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "invalid_symbol"}
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_invalid_pin(mock_keystore, hass):
"""Test a config flow initialized by the with invalid pin."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Invalid PIN."),
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "invalid_pin"}
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_expired_token(mock_keystore, hass):
"""Test a config flow initialized by the with expired token."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Expired token."),
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "expired_token"}
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_api_unknown_error(mock_keystore, hass):
"""Test a config flow with unknown API error."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=VulcanAPIException("Unknown error"),
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "unknown"}
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_api_unknown_runtime_error(mock_keystore, hass):
"""Test a config flow with runtime error."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=RuntimeError("Unknown error"),
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "unknown"}
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_connection_error(mock_keystore, hass):
"""Test a config flow with connection error."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=ClientConnectionError,
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S10000", CONF_REGION: "region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "cannot_connect"}
@mock.patch("homeassistant.components.vulcan.config_flow.Keystore.create")
async def test_config_flow_auth_unknown_error(mock_keystore, hass):
"""Test a config flow with unknown error."""
mock_keystore.return_value = fake_keystore
with patch(
"homeassistant.components.vulcan.config_flow.Account.register",
side_effect=Exception,
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] is None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_TOKEN: "3S10000", CONF_REGION: "invalid_region", CONF_PIN: "000000"},
)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "auth"
assert result["errors"] == {"base": "unknown"}
@mock.patch("homeassistant.components.vulcan.Vulcan.get_students")
async def test_options_flow(mock_student, hass):
"""Test config flow options."""
mock_student.return_value = [
Student.load(load_fixture("fake_student_1.json", "vulcan"))
]
config_entry = MockConfigEntry(
domain=const.DOMAIN,
unique_id="0",
data=json.loads(load_fixture("fake_config_entry_data.json", "vulcan")),
)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "init"
result = await hass.config_entries.options.async_configure(
result["flow_id"], user_input={CONF_SCAN_INTERVAL: 2137}
)
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
assert config_entry.options == {CONF_SCAN_INTERVAL: 2137}