Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 12 additions & 40 deletions homeassistant/components/vulcan/__init__.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
"""The Vulcan component."""
import logging

from aiohttp import ClientConnectorError
from vulcan import Account, Keystore, Vulcan
from vulcan._utils import VulcanAPIException
from vulcan import Account, Keystore, UnauthorizedCertificateException, Vulcan

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

PLATFORMS = ["calendar"]


Expand All @@ -22,54 +19,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
keystore = Keystore.load(entry.data["keystore"])
account = Account.load(entry.data["account"])
client = Vulcan(keystore, account)
client = Vulcan(keystore, account, async_get_clientsession(hass))
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 UnauthorizedCertificateException as err:
raise ConfigEntryAuthFailed("The certificate is not authorized.") from err
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)
)
raise ConfigEntryNotReady(
f"Connection error - please check your internet connection: {err}"
) from err
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)
)
hass.config_entries.async_setup_platforms(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""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

if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

async def _async_update_options(hass, entry):
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)
return unload_ok
167 changes: 49 additions & 118 deletions homeassistant/components/vulcan/calendar.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
"""Support for Vulcan Calendar platform."""
import copy
from __future__ import annotations

from datetime import date, datetime, timedelta
import logging

from aiohttp import ClientConnectorError
from vulcan._utils import VulcanAPIException
from vulcan import UnauthorizedCertificateException

from homeassistant.components.calendar import ENTITY_ID_FORMAT, CalendarEventDevice
from homeassistant.components.calendar import (
ENTITY_ID_FORMAT,
CalendarEntity,
CalendarEvent,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import generate_entity_id
from homeassistant.helpers.entity_platform import AddEntitiesCallback
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__)
Expand All @@ -29,20 +30,16 @@ async def async_setup_entry(
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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)
)
"""Set up the calendar platform for entity."""
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(
VulcanCalendarEntity(
client,
data,
generate_entity_id(
Expand All @@ -55,158 +52,93 @@ async def async_setup_entry(
)


class VulcanCalendarEventDevice(CalendarEventDevice):
"""A calendar event device."""
class VulcanCalendarEntity(CalendarEntity):
"""A calendar entity."""

def __init__(self, client, data, entity_id):
"""Create the Calendar event device."""
def __init__(self, client, data, entity_id) -> None:
"""Create the Calendar entity."""
self.student_info = data["student_info"]
self.data = VulcanCalendarData(
client,
self.student_info,
self.hass,
)
self._event = None
self._event: CalendarEvent | None = None
self.client = client
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_name = f"Vulcan calendar - {self.student_info['full_name']}"
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,
"name": f"{self.student_info['full_name']}: Calendar",
"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):
def event(self) -> CalendarEvent | None:
"""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):
async def async_get_events(self, hass, start_date, end_date) -> list[CalendarEvent]:
"""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 UnauthorizedCertificateException as err:
raise ConfigEntryAuthFailed(
"The certificate is not authorized, please authorize integration again"
) from err
except ClientConnectorError as err:
if self._available:
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 = CalendarEvent(
start=datetime.combine(item["date"], item["time"].from_),
end=datetime.combine(item["date"], item["time"].to),
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):
async def async_update(self) -> None:
"""Get the latest data."""

try:
events = await get_lessons(self.client)

if not self._available:
if not self.available:
_LOGGER.info("Restored connection with API")
self._available = True
self._attr_available = True

if events == []:
events = await get_lessons(
self.client,
date_to=date.today() + timedelta(days=7),
)
if events == []:
self.event = None
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 UnauthorizedCertificateException as err:
raise ConfigEntryAuthFailed(
"The certificate is not authorized, please authorize integration again"
) from err
except ClientConnectorError as err:
if self._available:
if self.available:
_LOGGER.warning(
"Connection error - please check your internet connection: %s", err
)
self._available = False
self._attr_available = False
return

new_event = min(
Expand All @@ -216,11 +148,10 @@ async def async_update(self):
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"],
}
self._event = CalendarEvent(
start=datetime.combine(new_event["date"], new_event["time"].from_),
end=datetime.combine(new_event["date"], new_event["time"].to),
summary=new_event["lesson"],
location=new_event["room"],
description=new_event["teacher"],
)
Loading