Lines 77.77% 21 / 27
Functions and Methods 42.85% 3 / 7
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
ManualEntityTimeTask 77.77% 21 / 27 42.85% 3 / 7 16.15 0.00% 0 / 1
 __construct 100.00% 5 / 5 100.00% 1 / 1 1
 getId 100.00% 1 / 1 100.00% 1 / 1 1
 cancel 100.00% 3 / 3 100.00% 1 / 1 1
 isActive 0.00% 0 / 1 0.00% 0 / 1 2
 getNextRunAt 0.00% 0 / 1 0.00% 0 / 1 6
 rearmFor 90.90% 10 / 11 0.00% 0 / 1 4.01
 createScheduleFor 40.00% 2 / 5 0.00% 0 / 1 7.46
1<?php
2
3declare(strict_types=1);
4
5namespace Stewart\Testing\Schedule;
6
7use Closure;
8use Stewart\Contracts\Entity\EntityId;
9use Stewart\Contracts\HaContext;
10use Stewart\Contracts\Schedule\ScheduledRun;
11use Stewart\Contracts\Schedule\ScheduledTask;
12use Stewart\Contracts\State\EntityState;
13use Stewart\Contracts\State\StateChange;
14use Stewart\Contracts\Subscription;
15use Stewart\Contracts\Time\Clock;
16use Stewart\Contracts\Time\Instant;
17use Stewart\Support\Schedule\CalendarSchedule;
18use Stewart\Support\Schedule\EntityTime;
19use Stewart\Support\Schedule\OneShotSchedule;
20use Stewart\Support\Schedule\WallClockSchedule;
21
22final class ManualEntityTimeTask implements ScheduledTask
23{
24    private EntityTime $armedTime;
25
26    private ?ScheduledTask $inner = null;
27
28    private readonly Subscription $following;
29
30    private bool $active = true;
31
32    /**
33     * @param Closure(WallClockSchedule, Closure(ScheduledRun): void): ScheduledTask $armTask
34     * @param Closure(ScheduledRun): void $handler
35     */
36    public function __construct(
37        private readonly string $id,
38        private readonly EntityId $entityId,
39        HaContext $ha,
40        private readonly Clock $clock,
41        private readonly Closure $armTask,
42        private readonly Closure $handler,
43    ) {
44        $this->armedTime = EntityTime::none();
45        $this->rearmFor($ha->getState($entityId));
46        $this->following = $ha->watchStateChanges($entityId)->subscribe(function (StateChange $change): void {
47            $this->rearmFor($change->to);
48        });
49    }
50
51    public function getId(): string
52    {
53        return $this->id;
54    }
55
56    public function cancel(): void
57    {
58        $this->active = false;
59        $this->inner?->cancel();
60        $this->following->unsubscribe();
61    }
62
63    public function isActive(): bool
64    {
65        return $this->active;
66    }
67
68    public function getNextRunAt(): ?Instant
69    {
70        return $this->active ? $this->inner?->getNextRunAt() : null;
71    }
72
73    private function rearmFor(?EntityState $state): void
74    {
75        $time = EntityTime::fromEntityState($state, $this->clock->getTimeZone());
76
77        if (!$this->active || $time->equals($this->armedTime)) {
78            return;
79        }
80
81        $this->inner?->cancel();
82        $this->inner = null;
83        $this->armedTime = $time;
84        $schedule = $this->createScheduleFor($time);
85
86        if ($schedule !== null) {
87            $this->inner = ($this->armTask)($schedule, function (ScheduledRun $run): void {
88                ($this->handler)(new ScheduledRun($this, $run->scheduledFor, $run->firedAt, $run->missedOccurrences));
89            });
90        }
91    }
92
93    private function createScheduleFor(EntityTime $time): ?WallClockSchedule
94    {
95        if ($time->dailyAt !== null) {
96            return CalendarSchedule::dailyAt($time->dailyAt);
97        }
98
99        if ($time->moment !== null && $time->moment->isAfter($this->clock->getNow())) {
100            return OneShotSchedule::fromMoment($time->moment->toDateTime($this->clock->getTimeZone()));
101        }
102
103        return null;
104    }
105}