Lines
97.82%
45 / 46
Functions and Methods
94.73%
18 / 19
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| EntityFilter | 97.82% | 45 / 46 | 94.73% | 18 / 19 | 35 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| inArea | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| onFloor | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| labelled | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| ofDevice | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| inDomain | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| matching | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| withArea | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| withFloor | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| withLabel | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| withDevice | 100.00% | 2 / 2 | 100.00% | 1 / 1 | 1 | |||||
| withDomain | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| withSelector | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 2 | |||||
| matchesEntity | 85.71% | 6 / 7 | 0.00% | 0 / 1 | 9.24 | |||||
| toCanonicalKey | 100.00% | 10 / 10 | 100.00% | 1 / 1 | 1 | |||||
| unconstrained | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| matchesPlacement | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 6 | |||||
| containsWhenConstrained | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 3 | |||||
| listSortedValues | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 1 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Stewart\Contracts\Registry; | |
| 6 | ||
| 7 | use Stewart\Contracts\Entity\EntityId; | |
| 8 | use Stewart\Contracts\Exception\IdentifierException; | |
| 9 | use Stewart\Contracts\Exception\SelectorException; | |
| 10 | use Stewart\Contracts\Registry\Collection\AreaIdCollection; | |
| 11 | use Stewart\Contracts\Registry\Collection\DeviceIdCollection; | |
| 12 | use Stewart\Contracts\Registry\Collection\FloorIdCollection; | |
| 13 | use Stewart\Contracts\Registry\Collection\LabelIdCollection; | |
| 14 | use Stewart\Contracts\Registry\Collection\RegistryIdCollection; | |
| 15 | use Stewart\Contracts\Selector\Collection\SelectorCollection; | |
| 16 | use Stewart\Contracts\Selector\Selector; | |
| 17 | ||
| 18 | final readonly class EntityFilter | |
| 19 | { | |
| 20 | /** @param list<string> $domains */ | |
| 21 | private function __construct( | |
| 22 | private AreaIdCollection $areaIds, | |
| 23 | private FloorIdCollection $floorIds, | |
| 24 | private LabelIdCollection $labelIds, | |
| 25 | private DeviceIdCollection $deviceIds, | |
| 26 | private array $domains, | |
| 27 | private ?Selector $selector, | |
| 28 | ) {} | |
| 29 | ||
| 30 | /** @throws IdentifierException */ | |
| 31 | public static function inArea(AreaId|string $areaId, AreaId|string ...$moreAreaIds): self | |
| 32 | { | |
| 33 | return self::unconstrained()->withArea($areaId, ...$moreAreaIds); | |
| 34 | } | |
| 35 | ||
| 36 | /** @throws IdentifierException */ | |
| 37 | public static function onFloor(FloorId|string $floorId, FloorId|string ...$moreFloorIds): self | |
| 38 | { | |
| 39 | return self::unconstrained()->withFloor($floorId, ...$moreFloorIds); | |
| 40 | } | |
| 41 | ||
| 42 | /** @throws IdentifierException */ | |
| 43 | public static function labelled(LabelId|string $labelId, LabelId|string ...$moreLabelIds): self | |
| 44 | { | |
| 45 | return self::unconstrained()->withLabel($labelId, ...$moreLabelIds); | |
| 46 | } | |
| 47 | ||
| 48 | /** @throws IdentifierException */ | |
| 49 | public static function ofDevice(DeviceId|string $deviceId, DeviceId|string ...$moreDeviceIds): self | |
| 50 | { | |
| 51 | return self::unconstrained()->withDevice($deviceId, ...$moreDeviceIds); | |
| 52 | } | |
| 53 | ||
| 54 | public static function inDomain(string $domain, string ...$moreDomains): self | |
| 55 | { | |
| 56 | return self::unconstrained()->withDomain($domain, ...$moreDomains); | |
| 57 | } | |
| 58 | ||
| 59 | /** @throws SelectorException */ | |
| 60 | public static function matching(string|EntityId|Selector|SelectorCollection $selector): self | |
| 61 | { | |
| 62 | return self::unconstrained()->withSelector($selector); | |
| 63 | } | |
| 64 | ||
| 65 | /** @throws IdentifierException */ | |
| 66 | public function withArea(AreaId|string ...$areaIds): self | |
| 67 | { | |
| 68 | $added = array_map(AreaId::fromStringOrId(...), $areaIds); | |
| 69 | ||
| 70 | return new self(AreaIdCollection::fromList([...$this->areaIds, ...$added]), $this->floorIds, $this->labelIds, $this->deviceIds, $this->domains, $this->selector); | |
| 71 | } | |
| 72 | ||
| 73 | /** @throws IdentifierException */ | |
| 74 | public function withFloor(FloorId|string ...$floorIds): self | |
| 75 | { | |
| 76 | $added = array_map(FloorId::fromStringOrId(...), $floorIds); | |
| 77 | ||
| 78 | return new self($this->areaIds, FloorIdCollection::fromList([...$this->floorIds, ...$added]), $this->labelIds, $this->deviceIds, $this->domains, $this->selector); | |
| 79 | } | |
| 80 | ||
| 81 | /** @throws IdentifierException */ | |
| 82 | public function withLabel(LabelId|string ...$labelIds): self | |
| 83 | { | |
| 84 | $added = array_map(LabelId::fromStringOrId(...), $labelIds); | |
| 85 | ||
| 86 | return new self($this->areaIds, $this->floorIds, LabelIdCollection::fromList([...$this->labelIds, ...$added]), $this->deviceIds, $this->domains, $this->selector); | |
| 87 | } | |
| 88 | ||
| 89 | /** @throws IdentifierException */ | |
| 90 | public function withDevice(DeviceId|string ...$deviceIds): self | |
| 91 | { | |
| 92 | $added = array_map(DeviceId::fromStringOrId(...), $deviceIds); | |
| 93 | ||
| 94 | return new self($this->areaIds, $this->floorIds, $this->labelIds, DeviceIdCollection::fromList([...$this->deviceIds, ...$added]), $this->domains, $this->selector); | |
| 95 | } | |
| 96 | ||
| 97 | public function withDomain(string ...$domains): self | |
| 98 | { | |
| 99 | return new self($this->areaIds, $this->floorIds, $this->labelIds, $this->deviceIds, array_values(array_unique([...$this->domains, ...$domains])), $this->selector); | |
| 100 | } | |
| 101 | ||
| 102 | /** @throws SelectorException */ | |
| 103 | public function withSelector(string|EntityId|Selector|SelectorCollection $selector): self | |
| 104 | { | |
| 105 | $added = Selector::fromSpec($selector); | |
| 106 | $combined = $this->selector === null ? $added : Selector::anyOf($this->selector, $added); | |
| 107 | ||
| 108 | return new self($this->areaIds, $this->floorIds, $this->labelIds, $this->deviceIds, $this->domains, $combined); | |
| 109 | } | |
| 110 | ||
| 111 | public function matchesEntity(EntityId $entityId, Registry $registry): bool | |
| 112 | { | |
| 113 | if ($this->domains !== [] && !\in_array($entityId->domain, $this->domains, true)) { | |
| 114 | return false; | |
| 115 | } | |
| 116 | ||
| 117 | if ($this->selector !== null && !$this->selector->matches($entityId->value)) { | |
| 118 | return false; | |
| 119 | } | |
| 120 | ||
| 121 | if ($this->areaIds->isEmpty() && $this->floorIds->isEmpty() && $this->labelIds->isEmpty() && $this->deviceIds->isEmpty()) { | |
| 122 | return true; | |
| 123 | } | |
| 124 | ||
| 125 | return $this->matchesPlacement($registry->findEntityPlacement($entityId)); | |
| 126 | } | |
| 127 | ||
| 128 | public function toCanonicalKey(): string | |
| 129 | { | |
| 130 | $domains = $this->domains; | |
| 131 | sort($domains); | |
| 132 | ||
| 133 | return 'entity-filter:' . json_encode([ | |
| 134 | 'areas' => $this->listSortedValues($this->areaIds), | |
| 135 | 'floors' => $this->listSortedValues($this->floorIds), | |
| 136 | 'labels' => $this->listSortedValues($this->labelIds), | |
| 137 | 'devices' => $this->listSortedValues($this->deviceIds), | |
| 138 | 'domains' => $domains, | |
| 139 | 'selector' => $this->selector?->toCanonicalKey(), | |
| 140 | ], \JSON_THROW_ON_ERROR); | |
| 141 | } | |
| 142 | ||
| 143 | private static function unconstrained(): self | |
| 144 | { | |
| 145 | return new self(AreaIdCollection::empty(), FloorIdCollection::empty(), LabelIdCollection::empty(), DeviceIdCollection::empty(), [], null); | |
| 146 | } | |
| 147 | ||
| 148 | // Home Assistant skips hidden and config or diagnostic entities reached through the registry. | |
| 149 | private function matchesPlacement(EntityPlacement $placement): bool | |
| 150 | { | |
| 151 | return $placement->indirectlyTargetable | |
| 152 | && $this->containsWhenConstrained($this->areaIds, $placement->areaId) | |
| 153 | && $this->containsWhenConstrained($this->floorIds, $placement->floorId) | |
| 154 | && $this->containsWhenConstrained($this->deviceIds, $placement->deviceId) | |
| 155 | && ($this->labelIds->isEmpty() || $this->labelIds->containsAnyOf($placement->labelIds)); | |
| 156 | } | |
| 157 | ||
| 158 | /** | |
| 159 | * @template T of RegistryId | |
| 160 | * @param RegistryIdCollection<T> $ids | |
| 161 | * @param T|null $id | |
| 162 | */ | |
| 163 | private function containsWhenConstrained(RegistryIdCollection $ids, ?RegistryId $id): bool | |
| 164 | { | |
| 165 | return $ids->isEmpty() || ($id !== null && $ids->contains($id)); | |
| 166 | } | |
| 167 | ||
| 168 | /** | |
| 169 | * @template T of RegistryId | |
| 170 | * @param RegistryIdCollection<T> $ids | |
| 171 | * @return list<string> | |
| 172 | */ | |
| 173 | private function listSortedValues(RegistryIdCollection $ids): array | |
| 174 | { | |
| 175 | $values = array_values(array_unique($ids->toStrings())); | |
| 176 | sort($values); | |
| 177 | ||
| 178 | return $values; | |
| 179 | } | |
| 180 | } |