Lines 73.07% 38 / 52
Methods 41.66% 5 / 12
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 fromArray 100.00% 12 / 12 100.00% 1 / 1 6
 onSunrise 0.00% 0 / 1 0.00% 0 / 1 2
 onSunset 0.00% 0 / 1 0.00% 0 / 1 2
 atTime 66.66% 4 / 6 0.00% 0 / 1 7.33
 onTimePattern 85.71% 6 / 7 0.00% 0 / 1 2.01
 whenTemplateTrue 33.33% 2 / 6 0.00% 0 / 1 5.67
 onZoneTransition 100.00% 9 / 9 100.00% 1 / 1 2
 toArray 100.00% 1 / 1 100.00% 1 / 1 1
 getPlatform 100.00% 3 / 3 100.00% 1 / 1 1
 createSunTrigger 0.00% 0 / 4 0.00% 0 / 1 12
 createWithId 0.00% 0 / 1 0.00% 0 / 1 6
14final readonly class HaTrigger
15{
16    private const string TIME_OF_DAY_PATTERN = '/\A([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?\z/';
17
18    private const array TIME_ENTITY_DOMAINS = ['input_datetime', 'sensor'];
19
20    /** @param array<string, mixed> $config */
21    private function __construct(private array $config) {}
22
23    /**
24     * @param array<array-key, mixed> $config
25     * @throws TriggerException
26     */
27    public static function fromArray(array $config): self
28    {
29        $platform = $config['trigger'] ?? $config['platform'] ?? null;
30
31        if (!\is_string($platform) || $platform === '') {
32            throw TriggerException::configInvalid();
33        }
34
35        $typedConfig = [];
36
37        foreach ($config as $key => $value) {
38            if (!\is_string($key)) {
39                throw TriggerException::configInvalid();
40            }
41
42            $typedConfig[$key] = $value;
43        }
44
45        $unencodablePath = TriggerJson::findUnencodablePath($typedConfig, 'trigger');
46
47        if ($unencodablePath !== null) {
48            throw TriggerException::configUnencodable($unencodablePath);
49        }
50
51        return new self($typedConfig);
52    }
53
54    public static function onSunrise(?SunOffset $offset = null, ?string $id = null): self
55    {
56        return self::createSunTrigger('sunrise', $offset, $id);
57    }
58
59    public static function onSunset(?SunOffset $offset = null, ?string $id = null): self
60    {
61        return self::createSunTrigger('sunset', $offset, $id);
62    }
63
64    /** @throws TriggerException */
65    public static function atTime(string|EntityId $time, ?string $id = null): self
66    {
67        if (\is_string($time) && preg_match(self::TIME_OF_DAY_PATTERN, $time) === 1) {
68            return self::createWithId(['trigger' => 'time', 'at' => $time], $id);
69        }
70
71        $entityId = \is_string($time) ? EntityId::tryFromString($time) : $time;
72
73        if ($entityId === null || !\in_array($entityId->domain, self::TIME_ENTITY_DOMAINS, true)) {
74            throw TriggerException::createForReason(TriggerError::TimeInvalid, time: (string) $time);
75        }
76
77        return self::createWithId(['trigger' => 'time', 'at' => $entityId->value], $id);
78    }
79
80    /** @throws TriggerException */
81    public static function onTimePattern(
82        int|string|null $hours = null,
83        int|string|null $minutes = null,
84        int|string|null $seconds = null,
85        ?string $id = null,
86    ): self {
87        $pattern = array_filter(
88            ['hours' => $hours, 'minutes' => $minutes, 'seconds' => $seconds],
89            static fn(int|string|null $part): bool => $part !== null,
90        );
91
92        if ($pattern === []) {
93            throw TriggerException::createForReason(TriggerError::TimePatternEmpty);
94        }
95
96        return self::createWithId(['trigger' => 'time_pattern', ...$pattern], $id);
97    }
98
99    /** @throws TriggerException */
100    public static function whenTemplateTrue(string $template, ?Duration $for = null, ?string $id = null): self
101    {
102        if (trim($template) === '') {
103            throw TriggerException::createForReason(TriggerError::TemplateEmpty);
104        }
105
106        $config = ['trigger' => 'template', 'value_template' => $template];
107
108        if ($for !== null) {
109            $config['for'] = $for->formatAsClock();
110        }
111
112        return self::createWithId($config, $id);
113    }
114
115    /** @throws TriggerException|IdentifierException */
116    public static function onZoneTransition(
117        EntityId|string $entity,
118        EntityId|string $zone,
119        ZoneTransition $transition,
120        ?string $id = null,
121    ): self {
122        $zoneId = EntityId::fromStringOrId($zone);
123
124        if ($zoneId->domain !== 'zone') {
125            throw TriggerException::createForReason(TriggerError::ZoneInvalid, zone: $zoneId->value);
126        }
127
128        return self::createWithId([
129            'trigger' => 'zone',
130            'entity_id' => EntityId::fromStringOrId($entity)->value,
131            'zone' => $zoneId->value,
132            'event' => $transition->value,
133        ], $id);
134    }
135
136    /** @return array<string, mixed> */
137    public function toArray(): array
138    {
139        return $this->config;
140    }
141
142    public function getPlatform(): string
143    {
144        $platform = $this->config['trigger'] ?? $this->config['platform'];
145        \assert(\is_string($platform));
146
147        return $platform;
148    }
149
150    private static function createSunTrigger(string $event, ?SunOffset $offset, ?string $id): self
151    {
152        $config = ['trigger' => 'sun', 'event' => $event];
153
154        if ($offset !== null && !$offset->isNone()) {
155            $config['offset'] = $offset->formatAsHaOffset();
156        }
157
158        return self::createWithId($config, $id);
159    }
160
161    /** @param array<string, mixed> $config */
162    private static function createWithId(array $config, ?string $id): self
163    {
164        return new self($id === null ? $config : [...$config, 'id' => $id]);
165    }
166}