Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

11 feature request sensoren über stundenplan generieren #19

Merged
merged 3 commits into from
Nov 22, 2024
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,7 @@ cython_debug/
#.idea/

# secrets
*creds.sec*
*creds.sec*

# test
test/
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
# homeassistantedupage
An HomeAssistant integration of the EduPage Schooling System based on the edupage_api library found here https://github.com/EdupageAPI/edupage-api

## Installation without HACS
* Extract files in /custom_components/homeassistantedupage to your installation.
* restart Home Assistant
* Add new integration and search for "Edupage"
* enter Username, Password and Subdomain (w/o ".edupage.org")
* based on your subjects you should find more or less sensors now, named bei the subject with grade-counts
* data is to be found as "attributes", see screenshot
# IMPORTANT
In this phase of development please remove integration after update and reinstall because there are major changes.

## Installation with HACS
* open HACS
Expand All @@ -17,13 +12,14 @@ An HomeAssistant integration of the EduPage Schooling System based on the edupag
* type "integration"
* add
* choose download
* please alway select at least a release with "HACS" in releasename
* please alway select the last one because its work in progress
* restart HA
* add integration
* look for "edupage"
* look for "edupage" with the nice "E" icon
* use "homeassistantedupage" integration
* enter login, password und (ONLY!) subdomain (no .edupage.com or something)
* if there are grades in your account there should spawn one ore more entities
* you should see now a lot of sensors with the subjects of your school
* grades are attributes if existing

![screenshot of sensors](./img/edupage_subjects_grades.jpg)

Expand Down
67 changes: 44 additions & 23 deletions custom_components/homeassistantedupage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import asyncio
from datetime import timedelta
import datetime
from edupage_api.exceptions import BadCredentialsException, CaptchaException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand All @@ -13,19 +14,13 @@

async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""only ConfigEntry supported, no configuration.yaml yet"""
_LOGGER.info("INIT called async_setup")
return True

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""create ConfigEntry an DataUpdateCoordinator"""
_LOGGER.info("called async_setup_entry")

username = entry.data["username"]
password = entry.data["password"]
subdomain = entry.data["subdomain"]
edupage = Edupage(hass)

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""initializin EduPage-integration and validate API-login"""
_LOGGER.info("INIT called async_setup_entry")

username = entry.data["username"]
password = entry.data["password"]
subdomain = entry.data["subdomain"]
Expand All @@ -36,32 +31,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
login_success = await hass.async_add_executor_job(
edupage.login, username, password, subdomain
)
_LOGGER.info("INIT login_success")

except BadCredentialsException as e:
_LOGGER.error("login failed: bad credentials. %s", e)
return False # stop initialization on any exception
_LOGGER.error("INIT login failed: bad credentials. %s", e)
return False

except CaptchaException as e:
_LOGGER.error("login failed: CAPTCHA needed. %s", e)
_LOGGER.error("INIT login failed: CAPTCHA needed. %s", e)
return False

except Exception as e:
_LOGGER.error("unexpected login error: %s", e)
_LOGGER.error("INIT unexpected login error: %s", e)
return False

fetch_lock = asyncio.Lock()

async def fetch_data():
"""function to fetch grade data."""
"""Function to fetch grade and timetable data."""
_LOGGER.info("INIT called fetch_data")
async with fetch_lock:

try:
# request classes
classes_data = await edupage.get_classes()
# _LOGGER.info("INIT classes count: " + str(len(classes_data)))

# request grades
grades_data = await edupage.get_grades()
_LOGGER.debug("grades_data: %s", grades_data) # Zeigt die Daten im Log
return grades_data
# _LOGGER.info("INIT grade count: " + str(len(grades_data)))

# request user_id
userid = await edupage.get_user_id()
# _LOGGER.info("INIT user_id: "+str(userid))

# request all possible subjects
subjects_data = await edupage.get_subjects()
# _LOGGER.info("INIT subject count: " + str(len(subjects_data)))

# request all possible students
students_data = await edupage.get_students()
# _LOGGER.info("INIT students count: " + str(len(students_data)))

return {
"grades": grades_data,
# "timetable": timetable_data,
"user_id": userid,
"subjects": subjects_data
}

except Exception as e:
_LOGGER.error("error fetching grades data: %s", e)
return []
_LOGGER.error("INIT error fetching data: %s", e)
return False

coordinator = DataUpdateCoordinator(
hass,
Expand All @@ -71,22 +91,23 @@ async def fetch_data():
update_interval=timedelta(minutes=5),
)

# first data fetch
# First data fetch
await asyncio.sleep(1)
await coordinator.async_config_entry_first_refresh()
#await coordinator.async_request_refresh()

# save coordinator
# Save coordinator
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator

# platforms forward
# Forward platforms
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload ConfigEntry."""
_LOGGER.info("INIT called async_unload_entry")
unload_ok = await hass.config_entries.async_forward_entry_unload(entry, Platform.SENSOR)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
Expand Down
37 changes: 30 additions & 7 deletions custom_components/homeassistantedupage/homeassistant_edupage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from homeassistant.helpers.update_coordinator import UpdateFailed

_LOGGER = logging.getLogger(__name__)

class Edupage:
def __init__(self,hass):
self.hass = hass
Expand All @@ -13,23 +14,45 @@ def login(self, username, password, subdomain):

return self.api.login(username, password, subdomain)

async def get_classes(self):

try:
classes_data = await self.hass.async_add_executor_job(self.api.get_classes)
return classes_data
except Exception as e:
raise UpdateFailed(F"EDUPAGE error updating get_classes() data from API: {e}")

async def get_grades(self):

try:
grades = await self.hass.async_add_executor_job(self.api.get_grades)
_LOGGER.debug("get_grades() successful from API")
return grades
except Exception as e:
raise UpdateFailed(F"error updating get_grades() data from API: {e}")
raise UpdateFailed(F"EDUPAGE error updating get_grades() data from API: {e}")

async def get_subjects(self):

try:
all_subjects = await self.hass.async_add_executor_job(self.api.get_subjects)
return all_subjects
except Exception as e:
raise UpdateFailed(F"EDUPAGE error updating get_subjects() data from API: {e}")

async def get_students(self):

try:
all_students = await self.hass.async_add_executor_job(self.api.get_students)
return all_students
except Exception as e:
raise UpdateFailed(F"EDUPAGE error updating get_students() data from API: {e}")

async def get_timetable(self, dateTT: datetime):
async def get_user_id(self):

try:
timetable = await self.hass.aync_add_executor_job(self.api.get_timetable())
_LOGGER.debug("get_timetable() successful from API")
return timetable
user_id_data = await self.hass.async_add_executor_job(self.api.get_user_id)
return user_id_data
except Exception as e:
raise UpdateFailed(F"error updating get_timetable() data from API: {e}")
raise UpdateFailed(F"EDUPAGE error updating get_user_id() data from API: {e}")

async def async_update(self):

Expand Down
58 changes: 41 additions & 17 deletions custom_components/homeassistantedupage/sensor.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,72 @@
import logging
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from collections import defaultdict

_LOGGER = logging.getLogger("custom_components.homeassistant_edupage")

def group_grades_by_subject(grades):
"""grouping grades based on subject_id."""
grouped = defaultdict(list)
for grade in grades:
grouped[grade.subject_id].append(grade)
return grouped

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
"""Set up EduPage sensors based on subjects in the data."""
"""Set up EduPage sensors based on subjects and their grades."""
_LOGGER.info("SENSOR called async_setup_entry")

coordinator = hass.data[DOMAIN][entry.entry_id]
subjects = {}
subjects = coordinator.data.get("subjects", [])
grades = coordinator.data.get("grades", [])

# Sortiere Noten nach Fächern
for grade in coordinator.data:
subject = grade.subject_name
if subject not in subjects:
subjects[subject] = []
subjects[subject].append(grade)
# group grades based on subject_id
grades_by_subject = group_grades_by_subject(grades)

sensors = []
for subject in subjects:
# get grades per subject based on subject_id
subject_grades = grades_by_subject.get(subject.subject_id, [])
sensor = EduPageSubjectSensor(
coordinator,
subject.name,
subject_grades
)
sensors.append(sensor)

# Erstelle für jedes Fach einen Sensor
sensors = [EduPageSubjectSensor(coordinator, subject, grades) for subject, grades in subjects.items()]
async_add_entities(sensors, True)

class EduPageSubjectSensor(CoordinatorEntity, SensorEntity):
"""Sensor-Entität für ein bestimmtes Unterrichtsfach."""
"""subject sensor entity."""

def __init__(self, coordinator, subject_name, grades):
"""Initialisierung des Fach-Sensors."""
def __init__(self, coordinator, subject_name, grades=None):
"""initializing"""
super().__init__(coordinator)
self._subject_name = subject_name
self._grades = grades
self._attr_name = f"EduPage Noten - {subject_name}" # Name des Sensors basierend auf dem Fach
self._grades = grades or []
self._attr_name = f"EduPage subject - {subject_name}"
self._attr_unique_id = f"edupage_grades_{subject_name.lower().replace(' ', '_')}"

@property
def state(self):
"""Gibt die Anzahl der Noten für dieses Fach zurück."""
"""return grade count"""
return len(self._grades)

@property
def extra_state_attributes(self):
"""Rückgabe zusätzlicher Attribute für den Sensor."""
"""return additional attributes"""
if not self._grades:
return {"info": "no grades yet"}

attributes = {}
for i, grade in enumerate(self._grades):
attributes[f"grade_{i+1}_title"] = grade.title
attributes[f"grade_{i+1}_grade_n"] = grade.grade_n
attributes[f"grade_{i+1}_date"] = grade.date.strftime("%Y-%m-%d %H:%M:%S")
attributes[f"grade_{i+1}_teacher"] = grade.teacher.name
return attributes

Loading