Lines 100.00% 41 / 41
Methods 100.00% 21 / 21
Classes 100.00% 1 / 1
Name Lines Methods CRAP
 __construct 100.00% 4 / 4 100.00% 1 / 1 2
 fromParts 100.00% 1 / 1 100.00% 1 / 1 1
 empty 100.00% 1 / 1 100.00% 1 / 1 1
 listAreas 100.00% 1 / 1 100.00% 1 / 1 1
 findArea 100.00% 1 / 1 100.00% 1 / 1 1
 findAreaByName 100.00% 1 / 1 100.00% 1 / 1 1
 listAreasOnFloor 100.00% 2 / 2 100.00% 1 / 1 1
 listFloors 100.00% 1 / 1 100.00% 1 / 1 1
 findFloor 100.00% 1 / 1 100.00% 1 / 1 1
 findFloorByName 100.00% 1 / 1 100.00% 1 / 1 1
 listLabels 100.00% 1 / 1 100.00% 1 / 1 1
 findLabel 100.00% 1 / 1 100.00% 1 / 1 1
 findLabelByName 100.00% 1 / 1 100.00% 1 / 1 1
 listDevices 100.00% 1 / 1 100.00% 1 / 1 1
 findDevice 100.00% 1 / 1 100.00% 1 / 1 1
 listDevicesInArea 100.00% 2 / 2 100.00% 1 / 1 1
 listEntities 100.00% 1 / 1 100.00% 1 / 1 1
 findEntity 100.00% 1 / 1 100.00% 1 / 1 1
 findEntityPlacement 100.00% 2 / 2 100.00% 1 / 1 1
 placeEntity 100.00% 11 / 11 100.00% 1 / 1 4
 mergeLabelIds 100.00% 5 / 5 100.00% 1 / 1 3
27final readonly class IndexedRegistry implements Registry
28{
29    /** @var array<string, EntityPlacement> */
30    private array $placements;
31
32    private function __construct(
33        private AreaCollection $areas,
34        private FloorCollection $floors,
35        private LabelCollection $labels,
36        private DeviceCollection $devices,
37        private RegisteredEntityCollection $entities,
38    ) {
39        $placements = [];
40
41        foreach ($entities as $entity) {
42            $placements[$entity->entityId->value] = $this->placeEntity($entity);
43        }
44
45        $this->placements = $placements;
46    }
47
48    public static function fromParts(
49        AreaCollection $areas,
50        FloorCollection $floors,
51        LabelCollection $labels,
52        DeviceCollection $devices,
53        RegisteredEntityCollection $entities,
54    ): self {
55        return new self($areas, $floors, $labels, $devices, $entities);
56    }
57
58    public static function empty(): self
59    {
60        return new self(AreaCollection::empty(), FloorCollection::empty(), LabelCollection::empty(), DeviceCollection::empty(), RegisteredEntityCollection::empty());
61    }
62
63    public function listAreas(): AreaCollection
64    {
65        return $this->areas;
66    }
67
68    public function findArea(AreaId|string $areaId): ?Area
69    {
70        return $this->areas->find(AreaId::fromStringOrId($areaId));
71    }
72
73    public function findAreaByName(string $name): ?Area
74    {
75        return $this->areas->findFirstWhere(static fn(Area $area): bool => $area->isNamed($name));
76    }
77
78    public function listAreasOnFloor(FloorId|string $floorId): AreaCollection
79    {
80        $floorId = FloorId::fromStringOrId($floorId);
81
82        return $this->areas->filter(static fn(Area $area): bool => $area->floorId?->equals($floorId) ?? false);
83    }
84
85    public function listFloors(): FloorCollection
86    {
87        return $this->floors;
88    }
89
90    public function findFloor(FloorId|string $floorId): ?Floor
91    {
92        return $this->floors->find(FloorId::fromStringOrId($floorId));
93    }
94
95    public function findFloorByName(string $name): ?Floor
96    {
97        return $this->floors->findFirstWhere(static fn(Floor $floor): bool => $floor->isNamed($name));
98    }
99
100    public function listLabels(): LabelCollection
101    {
102        return $this->labels;
103    }
104
105    public function findLabel(LabelId|string $labelId): ?Label
106    {
107        return $this->labels->find(LabelId::fromStringOrId($labelId));
108    }
109
110    public function findLabelByName(string $name): ?Label
111    {
112        return $this->labels->findFirstWhere(static fn(Label $label): bool => $label->isNamed($name));
113    }
114
115    public function listDevices(): DeviceCollection
116    {
117        return $this->devices;
118    }
119
120    public function findDevice(DeviceId|string $deviceId): ?Device
121    {
122        return $this->devices->find(DeviceId::fromStringOrId($deviceId));
123    }
124
125    public function listDevicesInArea(AreaId|string $areaId): DeviceCollection
126    {
127        $areaId = AreaId::fromStringOrId($areaId);
128
129        return $this->devices->filter(static fn(Device $device): bool => $device->areaId?->equals($areaId) ?? false);
130    }
131
132    public function listEntities(): RegisteredEntityCollection
133    {
134        return $this->entities;
135    }
136
137    public function findEntity(EntityId|string $entityId): ?RegisteredEntity
138    {
139        return $this->entities->find(EntityId::fromStringOrId($entityId));
140    }
141
142    public function findEntityPlacement(EntityId|string $entityId): EntityPlacement
143    {
144        $entityId = EntityId::fromStringOrId($entityId);
145
146        return $this->placements[$entityId->value] ?? EntityPlacement::unregistered($entityId);
147    }
148
149    // Mirrors Home Assistant target resolution: an entity's own area overrides its device's.
150    private function placeEntity(RegisteredEntity $entity): EntityPlacement
151    {
152        $device = $entity->deviceId === null ? null : $this->devices->find($entity->deviceId);
153        $areaId = $entity->areaId ?? $device?->areaId;
154        $area = $areaId === null ? null : $this->areas->find($areaId);
155
156        return new EntityPlacement(
157            entityId: $entity->entityId,
158            deviceId: $entity->deviceId,
159            areaId: $areaId,
160            floorId: $area?->floorId,
161            labelIds: $this->mergeLabelIds($entity->labelIds, $device->labelIds ?? [], $area->labelIds ?? []),
162            indirectlyTargetable: !$entity->isHidden() && !$entity->hasEntityCategory(),
163        );
164    }
165
166    /** @param list<LabelId> ...$labelIdLists */
167    private function mergeLabelIds(array ...$labelIdLists): LabelIdCollection
168    {
169        $unique = [];
170
171        foreach ($labelIdLists as $labelIds) {
172            foreach ($labelIds as $labelId) {
173                $unique[$labelId->value] = $labelId;
174            }
175        }
176
177        return LabelIdCollection::fromList($unique);
178    }
179}