Improved calendar performance

This commit is contained in:
Tim Lappe 2025-03-23 13:25:45 +01:00
parent 0fbc7ddd20
commit 603bf0a5a2
7 changed files with 109 additions and 22 deletions

View File

@ -1,4 +1,4 @@
FROM php:8.2-cli FROM php:8.2-cli as base
# Install system dependencies # Install system dependencies
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
@ -21,14 +21,15 @@ RUN curl -sS https://get.symfony.com/cli/installer | bash && \
# Set working directory # Set working directory
WORKDIR /var/www/html WORKDIR /var/www/html
CMD ["symfony", "serve", "--port=8000", "--no-tls", "--allow-http", "--allow-all-ip"]
FROM base as production
# Copy application files # Copy application files
COPY . . COPY . .
# Install dependencies # Install dependencies
RUN composer install --no-interaction --optimize-autoloader RUN composer install --no-interaction --optimize-autoloader
# Expose port
EXPOSE 8000
# Command to run Symfony server # Command to run Symfony server
CMD ["symfony", "serve", "--port=8000", "--no-tls", "--allow-http", "--allow-all-ip"] CMD ["symfony", "serve", "--port=8000", "--no-tls", "--allow-http", "--allow-all-ip"]

View File

@ -5,8 +5,9 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: production
environment: environment:
- APP_ENV=dev - APP_ENV=prod
- APP_DEBUG=1 - APP_DEBUG=1
networks: networks:
- proxy - proxy

View File

@ -3,6 +3,11 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: base
ports:
- 8000:8000
environment: environment:
- APP_ENV=dev - APP_ENV=dev
- APP_DEBUG=1 - APP_DEBUG=1
volumes:
- ./:/var/www/html

View File

@ -2,10 +2,12 @@
namespace App\Controller; namespace App\Controller;
use App\HomeAssistant\HomeAssistantClient;
use App\Service\AbsenceManager; use App\Service\AbsenceManager;
use App\Service\CalendarExportService; use App\Service\CalendarExportService;
use App\Service\CalendarService; use App\Service\CalendarService;
use DateTime; use DateTime;
use DateTimeImmutable;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -17,6 +19,7 @@ final class CalendarController extends AbstractController
private readonly AbsenceManager $absenceManager, private readonly AbsenceManager $absenceManager,
private readonly CalendarService $calendarService, private readonly CalendarService $calendarService,
private readonly CalendarExportService $calendarExportService, private readonly CalendarExportService $calendarExportService,
private readonly HomeAssistantClient $homeAssistantClient,
private readonly string $requiredApiKey private readonly string $requiredApiKey
) { ) {
} }
@ -58,11 +61,17 @@ final class CalendarController extends AbstractController
$days = $this->calendarService->getAllDays($startDate, $endDate); $days = $this->calendarService->getAllDays($startDate, $endDate);
$days = $this->calendarService->processAbsences($days, $absences); $days = $this->calendarService->processAbsences($days, $absences);
$icsContent = $this->calendarExportService->generateIcsContent($days, $absences); $entityHistory = $this->homeAssistantClient->getEntityStateHistory(
'person.tim',
new DateTimeImmutable('-1 year'),
new DateTimeImmutable('+180 days')
);
$icsContent = $this->calendarExportService->generateIcsContent($days, $entityHistory, $absences);
$response = new Response($icsContent); $response = new Response($icsContent);
$response->headers->set('Content-Type', 'text/calendar; charset=utf-8'); //$response->headers->set('Content-Type', 'text/calendar; charset=utf-8');
$response->headers->set('Content-Disposition', 'attachment; filename="work_calendar.ics"'); //$response->headers->set('Content-Disposition', 'attachment; filename="work_calendar.ics"');
return $response; return $response;
} }

View File

@ -46,7 +46,7 @@ final readonly class HomeAssistantClient
'filter_entity_id' => $entityId, 'filter_entity_id' => $entityId,
]); ]);
return $this->request('GET', "/api/history/period/{$startDate->format('Y-m-d\TH:i:s\Z')}?{$queryParams}"); return $this->request('GET', "/api/history/period/{$startDate->format('Y-m-d\TH:i:s\Z')}?{$queryParams}")[0];
} }
public function callService(string $domain, string $service, array $data = []): array public function callService(string $domain, string $service, array $data = []): array

View File

@ -14,7 +14,6 @@ final class CalendarExportService
private const KAMEN_ADDRESS = 'Privat \nHeidkamp 21\n59174 Kamen\nDeutschland'; private const KAMEN_ADDRESS = 'Privat \nHeidkamp 21\n59174 Kamen\nDeutschland';
private const COMPANY_NAME = 'CHECK24'; private const COMPANY_NAME = 'CHECK24';
private const TIMEZONE = 'Europe/Berlin'; private const TIMEZONE = 'Europe/Berlin';
private const PERSON_ENTITY = 'person.tim';
private const OFFICE_STATE = 'CHECK24'; private const OFFICE_STATE = 'CHECK24';
public function __construct( public function __construct(
@ -22,7 +21,7 @@ final class CalendarExportService
) { ) {
} }
public function generateIcsContent(array $days, array $absences): string public function generateIcsContent(array $days, array $entityHistory, array $absences): string
{ {
$icsContent = [ $icsContent = [
'BEGIN:VCALENDAR', 'BEGIN:VCALENDAR',
@ -39,7 +38,14 @@ final class CalendarExportService
} }
$dayDate = clone $day['date']; $dayDate = clone $day['date'];
$startAndEndTimes = $this->getWorkTimesFromHomeAssistant($dayDate); $startOfDay = DateTimeImmutable::createFromInterface($dayDate)->setTime(0, 0, 0);
$typicalEndOfDay = DateTimeImmutable::createFromInterface($dayDate)->setTime(17, 30, 0);
$filteredEntityHistory = array_filter($entityHistory, function (array $state) use ($startOfDay, $typicalEndOfDay) {
return $state['last_changed'] >= $startOfDay && $state['last_changed'] <= $typicalEndOfDay;
});
$startAndEndTimes = $this->getWorkTimesFromHomeAssistant($filteredEntityHistory, $dayDate);
if ($startAndEndTimes === null) { if ($startAndEndTimes === null) {
$eventStart = clone $day['date']; $eventStart = clone $day['date'];
@ -74,17 +80,9 @@ final class CalendarExportService
return implode("\r\n", $icsContent); return implode("\r\n", $icsContent);
} }
private function getWorkTimesFromHomeAssistant(DateTimeInterface $date): ?array private function getWorkTimesFromHomeAssistant(array $stateHistory, DateTimeInterface $date): ?array
{ {
$startOfDay = DateTimeImmutable::createFromInterface($date)->setTime(0, 0, 0);
$typicalEndOfDay = DateTimeImmutable::createFromInterface($date)->setTime(17, 30, 0);
try { try {
$stateHistory = $this->homeAssistantClient->getEntityStateHistory(
self::PERSON_ENTITY,
$startOfDay,
$typicalEndOfDay
);
if (empty($stateHistory) || empty($stateHistory[0])) { if (empty($stateHistory) || empty($stateHistory[0])) {
return null; return null;

View File

@ -0,0 +1,73 @@
<?php
namespace App\Service;
use DateTime;
use RuntimeException;
final class CalendarStorageService
{
private const STORAGE_FILE = 'storage/past_events.json';
/**
* Store past events in the storage file
*/
public function storePastEvents(array $events): void
{
$dataDirectory = dirname(self::STORAGE_FILE);
if (!is_dir($dataDirectory)) {
if (mkdir($dataDirectory, 0777, true) === false) {
throw new RuntimeException('Failed to create storage directory');
}
}
if (file_put_contents(self::STORAGE_FILE, json_encode($events)) === false) {
throw new RuntimeException('Failed to write past events to storage file');
}
}
/**
* Get past events from the storage file
*/
public function getPastEvents(): array
{
if (!file_exists(self::STORAGE_FILE)) {
return [];
}
$content = file_get_contents(self::STORAGE_FILE);
if ($content === false) {
return [];
}
$events = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return [];
}
return $events;
}
/**
* Check if past events need to be updated by comparing with cutoff date
*/
public function shouldUpdatePastEvents(DateTime $cutoffDate): bool
{
if (!file_exists(self::STORAGE_FILE)) {
return true;
}
$fileModTime = filemtime(self::STORAGE_FILE);
if ($fileModTime === false) {
return true;
}
$fileModDate = (new DateTime())->setTimestamp($fileModTime);
return $fileModDate < $cutoffDate;
}
}