Lines 87.09% 27 / 31
Methods 83.33% 5 / 6
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 3 / 3 100.00% 1 / 1 1
 openSession 100.00% 3 / 3 100.00% 1 / 1 1
 relinkAfterSessionEnded 100.00% 10 / 10 100.00% 1 / 1 3
 markLinkLost 100.00% 3 / 3 100.00% 1 / 1 1
 relinkUntilSettled 55.55% 5 / 9 0.00% 0 / 1 9.16
 hasSettled 100.00% 3 / 3 100.00% 1 / 1 3
20final class ComponentSession
21{
22    private ?DeferredCancellation $relinking = null;
23
24    public function __construct(
25        private readonly ComponentLink $link,
26        private readonly ExposureLink $exposures,
27        private readonly ExposureReconciler $reconciler,
28        private readonly ComponentTracker $tracker,
29        private readonly StewartConfig $config,
30        private readonly Deadlines $deadlines,
31        private readonly LoggerInterface $logger,
32    ) {
33        $link->onCommand($exposures->receiveCommand(...));
34        $link->onSessionEnded($this->relinkAfterSessionEnded(...));
35        $exposures->onSessionLost($this->relinkAfterSessionEnded(...));
36    }
37
38    /** @throws HaClientException */
39    public function openSession(): void
40    {
41        $this->link->establishLink();
42        $this->exposures->replayAll();
43        $this->reconciler->reconcileIfDue();
44    }
45
46    public function relinkAfterSessionEnded(): void
47    {
48        // Also when a relink runs, so a session lost during its replay keeps it going.
49        $this->link->markLinkLost();
50
51        if ($this->relinking !== null) {
52            return;
53        }
54
55        $this->logger->info('The stewart Home Assistant integration session ended; opening a new one');
56        $relinking = $this->relinking = new DeferredCancellation();
57
58        async(function () use ($relinking): void {
59            try {
60                $this->relinkUntilSettled($relinking->getCancellation());
61            } finally {
62                if ($this->relinking === $relinking) {
63                    $this->relinking = null;
64                }
65            }
66        })->ignore();
67    }
68
69    public function markLinkLost(): void
70    {
71        $this->relinking?->cancel();
72        $this->relinking = null;
73        $this->link->markLinkLost();
74    }
75
76    private function relinkUntilSettled(Cancellation $stop): void
77    {
78        for ($attempt = 1; ; ++$attempt) {
79            try {
80                $this->openSession();
81            } catch (HaClientException $e) {
82                $this->logger->debug('Could not open the stewart integration session yet', ['exception' => $e, 'attempt' => $attempt]);
83            }
84
85            if ($stop->isRequested() || $this->hasSettled()) {
86                return;
87            }
88
89            try {
90                $this->deadlines->delay($this->config->reconnectBackoff->delayFor($attempt), $stop);
91            } catch (CancelledException) {
92                return;
93            }
94        }
95    }
96
97    // A missing component is usually one that Home Assistant is reloading.
98    private function hasSettled(): bool
99    {
100        return match ($this->tracker->detection->state) {
101            ComponentState::Unchecked, ComponentState::Missing => false,
102            ComponentState::ProtocolMismatch, ComponentState::Refused, ComponentState::Replaced, ComponentState::Active => true,
103        };
104    }
105}