Lines
100.00%
40 / 40
Methods
100.00%
11 / 11
Classes
100.00%
1 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| none | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| forDailyTime | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| forMoment | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| requireSupportedEntity | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 3 | ||
| fromEntityState | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 6 | ||
| equals | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 6 | ||
| fromInputDatetime | 100.00% | 8 / 8 | 100.00% | 1 / 1 | 7 | ||
| fromLocalDateAndTime | 100.00% | 6 / 6 | 100.00% | 1 / 1 | 5 | ||
| tryParseTimeOfDay | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | ||
| fromTimestampSensor | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 3 | ||
| 17 | final readonly class EntityTime | |
| 18 | { | |
| 19 | private const string INPUT_DATETIME_DOMAIN = 'input_datetime'; | |
| 20 | ||
| 21 | private const string SENSOR_DOMAIN = 'sensor'; | |
| 22 | ||
| 23 | private const string TIMESTAMP_DEVICE_CLASS = 'timestamp'; | |
| 24 | ||
| 25 | private const string DATE_AND_TIME_PATTERN = '/\A(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})\z/'; | |
| 26 | ||
| 27 | private const string TIME_ONLY_PATTERN = '/\A\d{2}:\d{2}:\d{2}\z/'; | |
| 28 | ||
| 29 | private function __construct( | |
| 30 | public ?TimeOfDay $dailyAt = null, | |
| 31 | public ?Instant $moment = null, | |
| 32 | ) {} | |
| 33 | ||
| 34 | public static function none(): self | |
| 35 | { | |
| 36 | return new self(); | |
| 37 | } | |
| 38 | ||
| 39 | public static function forDailyTime(TimeOfDay $time): self | |
| 40 | { | |
| 41 | return new self(dailyAt: $time); | |
| 42 | } | |
| 43 | ||
| 44 | public static function forMoment(Instant $moment): self | |
| 45 | { | |
| 46 | return new self(moment: $moment); | |
| 47 | } | |
| 48 | ||
| 49 | /** @throws ScheduleException */ | |
| 50 | public static function requireSupportedEntity(EntityId $entityId): void | |
| 51 | { | |
| 52 | if ($entityId->domain !== self::INPUT_DATETIME_DOMAIN && $entityId->domain !== self::SENSOR_DOMAIN) { | |
| 53 | throw ScheduleException::createForReason(ScheduleError::EntityTimeDomainUnsupported, entityId: $entityId->value); | |
| 54 | } | |
| 55 | } | |
| 56 | ||
| 57 | public static function fromEntityState(?EntityState $state, DateTimeZone $zone): self | |
| 58 | { | |
| 59 | if ($state === null || $state->isUnavailable()) { | |
| 60 | return self::none(); | |
| 61 | } | |
| 62 | ||
| 63 | return match ($state->getDomain()) { | |
| 64 | self::INPUT_DATETIME_DOMAIN => self::fromInputDatetime($state, $zone), | |
| 65 | self::SENSOR_DOMAIN => self::fromTimestampSensor($state), | |
| 66 | default => self::none(), | |
| 67 | }; | |
| 68 | } | |
| 69 | ||
| 70 | public function equals(self $other): bool | |
| 71 | { | |
| 72 | $sameDailyAt = $this->dailyAt === null || $other->dailyAt === null | |
| 73 | ? $this->dailyAt === $other->dailyAt | |
| 74 | : $this->dailyAt->equals($other->dailyAt); | |
| 75 | $sameMoment = $this->moment === null || $other->moment === null | |
| 76 | ? $this->moment === $other->moment | |
| 77 | : $this->moment->equals($other->moment); | |
| 78 | ||
| 79 | return $sameDailyAt && $sameMoment; | |
| 80 | } | |
| 81 | ||
| 82 | private static function fromInputDatetime(EntityState $state, DateTimeZone $zone): self | |
| 83 | { | |
| 84 | $hasDate = $state->getBoolAttribute('has_date'); | |
| 85 | $hasTime = $state->getBoolAttribute('has_time'); | |
| 86 | ||
| 87 | if ($hasDate !== false && preg_match(self::DATE_AND_TIME_PATTERN, $state->state, $matches) === 1) { | |
| 88 | return self::fromLocalDateAndTime($matches[1], $matches[2], $zone); | |
| 89 | } | |
| 90 | ||
| 91 | if ($hasDate !== true && $hasTime !== false && preg_match(self::TIME_ONLY_PATTERN, $state->state) === 1) { | |
| 92 | $time = self::tryParseTimeOfDay($state->state); | |
| 93 | ||
| 94 | return $time === null ? self::none() : self::forDailyTime($time); | |
| 95 | } | |
| 96 | ||
| 97 | return self::none(); | |
| 98 | } | |
| 99 | ||
| 100 | private static function fromLocalDateAndTime(string $date, string $time, DateTimeZone $zone): self | |
| 101 | { | |
| 102 | $day = DateTimeImmutable::createFromFormat('!Y-m-d', $date, $zone); | |
| 103 | $timeOfDay = self::tryParseTimeOfDay($time); | |
| 104 | ||
| 105 | if ($day === false || $day->format('Y-m-d') !== $date || $timeOfDay === null) { | |
| 106 | return self::none(); | |
| 107 | } | |
| 108 | ||
| 109 | $moment = $timeOfDay->resolveOnDay($day, $zone); | |
| 110 | ||
| 111 | return $moment === null ? self::none() : self::forMoment(Instant::fromDateTime($moment)); | |
| 112 | } | |
| 113 | ||
| 114 | private static function tryParseTimeOfDay(string $time): ?TimeOfDay | |
| 115 | { | |
| 116 | try { | |
| 117 | return TimeOfDay::parse($time); | |
| 118 | } catch (ScheduleException) { | |
| 119 | return null; | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | private static function fromTimestampSensor(EntityState $state): self | |
| 124 | { | |
| 125 | if ($state->getStringAttribute('device_class') !== self::TIMESTAMP_DEVICE_CLASS) { | |
| 126 | return self::none(); | |
| 127 | } | |
| 128 | ||
| 129 | $moment = Instant::tryFromIso($state->state); | |
| 130 | ||
| 131 | return $moment === null ? self::none() : self::forMoment($moment); | |
| 132 | } | |
| 133 | } |