strolap-calendar/src/Service/CalendarExportService.php
2025-03-15 09:54:07 +01:00

173 lines
5.9 KiB
PHP

<?php
namespace App\Service;
use App\HomeAssistant\HomeAssistantClient;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use RuntimeException;
final class CalendarExportService
{
private const string DUSSELDORF_ADDRESS = 'CHECK24 \nHeinricht-Heine-Allee 53\n40213 Düsseldorf\nDeutschland';
private const string KAMEN_ADDRESS = 'Privat \nHeidkamp 21\n59174 Kamen\nDeutschland';
private const string COMPANY_NAME = 'CHECK24';
private const string TIMEZONE = 'Europe/Berlin';
private const string PERSON_ENTITY = 'person.tim';
private const string OFFICE_STATE = 'CHECK24';
public function __construct(
private readonly HomeAssistantClient $homeAssistantClient
) {
}
public function generateIcsContent(array $days, array $absences): string
{
$icsContent = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//CHECK24//Work Calendar//EN',
'CALSCALE:GREGORIAN',
'METHOD:PUBLISH',
$this->generateTimezone(),
];
foreach ($days as $day) {
if ($this->isWeekend($day) || ($day['isUrlaub'] ?? false)) {
continue;
}
$dayDate = clone $day['date'];
$startAndEndTimes = $this->getWorkTimesFromHomeAssistant($dayDate);
if ($startAndEndTimes === null) {
$eventStart = clone $day['date'];
$eventStart->setTime(9, 0, 0);
$eventEnd = clone $day['date'];
$eventEnd->setTime(17, 30, 0);
} else {
[$eventStart, $eventEnd] = $startAndEndTimes;
}
$isHomeOffice = $day['isFtk'] ?? false;
$location = $isHomeOffice ? self::KAMEN_ADDRESS : self::DUSSELDORF_ADDRESS;
$name = match ($isHomeOffice) {
true => 'CHECK24 🏠',
false => 'CHECK24 🏢',
};
$icsContent[] = 'BEGIN:VEVENT';
$icsContent[] = 'UID:' . $this->generateUid($eventStart);
$icsContent[] = 'DTSTAMP:' . $this->formatDateTime(new DateTimeImmutable('now', new DateTimeZone(self::TIMEZONE)));
$icsContent[] = 'DTSTART;TZID=' . self::TIMEZONE . ':' . $this->formatDateTimeLocal($eventStart);
$icsContent[] = 'DTEND;TZID=' . self::TIMEZONE . ':' . $this->formatDateTimeLocal($eventEnd);
$icsContent[] = 'SUMMARY:' . $name;
$icsContent[] = 'LOCATION:' . $location;
$icsContent[] = 'END:VEVENT';
}
$icsContent[] = 'END:VCALENDAR';
return implode("\r\n", $icsContent);
}
private function getWorkTimesFromHomeAssistant(DateTimeInterface $date): ?array
{
$startOfDay = DateTimeImmutable::createFromInterface($date)->setTime(0, 0, 0);
$typicalEndOfDay = DateTimeImmutable::createFromInterface($date)->setTime(17, 30, 0);
try {
$stateHistory = $this->homeAssistantClient->getEntityStateHistory(
self::PERSON_ENTITY,
$startOfDay,
$typicalEndOfDay
);
if (empty($stateHistory) || empty($stateHistory[0])) {
return null;
}
$startTime = null;
$endTime = null;
$states = $stateHistory[0];
foreach ($states as $i => $state) {
if ($state['state'] === self::OFFICE_STATE) {
$startTime = new DateTimeImmutable($state['last_changed']);
for ($j = $i + 1; $j < count($states); $j++) {
if ($states[$j]['state'] !== self::OFFICE_STATE) {
$endTime = new DateTimeImmutable($states[$j]['last_changed']);
break;
}
}
if ($startTime !== null && $endTime === null) {
$endTime = clone $startTime;
$endTime = $endTime->modify('+9 hours');
}
break;
}
}
if ($startTime === null || $endTime === null) {
return null;
}
$startTime = $startTime->setTimezone(new DateTimeZone('Europe/Berlin'));
$endTime = $endTime->setTimezone(new DateTimeZone('Europe/Berlin'));
return [$startTime, $endTime];
} catch (RuntimeException) {
return null;
}
}
private function isWeekend(array $day): bool
{
return $day['weekday'] >= 6;
}
private function formatDateTime(DateTimeImmutable $dateTime): string
{
return $dateTime->setTimezone(new DateTimeZone('UTC'))->format('Ymd\THis\Z');
}
private function formatDateTimeLocal(DateTimeInterface $dateTime): string
{
return $dateTime->format('Ymd\THis');
}
private function generateUid(DateTimeInterface $dateTime): string
{
return $dateTime->format('Ymd') . '-' . md5($dateTime->format('Ymd') . '-CHECK24') . '@check24.de';
}
private function generateTimezone(): string
{
return implode("\r\n", [
'BEGIN:VTIMEZONE',
'TZID:' . self::TIMEZONE,
'X-LIC-LOCATION:' . self::TIMEZONE,
'BEGIN:DAYLIGHT',
'TZOFFSETFROM:+0100',
'TZOFFSETTO:+0200',
'TZNAME:CEST',
'DTSTART:19700329T020000',
'RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU',
'END:DAYLIGHT',
'BEGIN:STANDARD',
'TZOFFSETFROM:+0200',
'TZOFFSETTO:+0100',
'TZNAME:CET',
'DTSTART:19701025T030000',
'RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU',
'END:STANDARD',
'END:VTIMEZONE',
]);
}
}