Areas, floors and labels
Entity IDs say what a thing is. Home Assistant’s areas, floors and labels say where it is and what it is for. An
EntityFilter picks entities by them. This app turns on an area’s lights when a motion sensor in that area fires:
<?php
declare(strict_types=1);
namespace App;
use Stewart\Contracts\App;use Stewart\Contracts\Automation;use Stewart\Contracts\HaContext;use Stewart\Contracts\Registry\EntityFilter;use Stewart\Contracts\Service\ServiceTarget;
#[Automation(id: 'room-motion')]final readonly class RoomMotion implements App{ public function __construct( private HaContext $ha, private string $area = 'kitchen', ) {}
public function initialize(): void { $this->ha->watchStateChanges(EntityFilter::inArea($this->area)->withDomain('binary_sensor')->withLabel('motion')) ->whenChangedTo('on') ->subscribe(function (): void { $this->ha->callService('light', 'turn_on', target: ServiceTarget::forAreas($this->area)); }); }
public function dispose(): void {}}A filter works wherever entities are chosen: watchStateChanges() and listStates(). Add a new motion sensor to the
kitchen in Home Assistant, label it motion, and the app uses it without a change.
Building a filter
Section titled “Building a filter”Start with one condition and add more with with…():
| Start | Add | Matches |
|---|---|---|
EntityFilter::inArea('kitchen') |
->withArea(…) |
The entity’s area |
EntityFilter::onFloor('upstairs') |
->withFloor(…) |
The floor of that area |
EntityFilter::labelled('night') |
->withLabel(…) |
A label on the entity, its device or its area |
EntityFilter::ofDevice($deviceId) |
->withDevice(…) |
The entity’s device |
EntityFilter::inDomain('light') |
->withDomain(…) |
The part of the ID before the dot |
EntityFilter::matching('sensor.*_battery') |
->withSelector(…) |
An ID or pattern |
Several values in one condition mean “any of these”; all conditions must match.
EntityFilter::inArea('kitchen', 'hall')->withDomain('light')->withLabel('night') is a light in the kitchen or the
hall, labelled night.
Areas, floors, labels and devices are named by their ID, not their display name: kitchen, not Kitchen. Find
an ID in Home Assistant under Settings → Areas, labels & zones, or look it up by name (below). After
make generate, PhpStorm completes the IDs.
How Home Assistant places an entity
Section titled “How Home Assistant places an entity”Stewart follows Home Assistant’s rules, so a filter and ServiceTarget::forAreas('kitchen') reach the same entities:
- An entity’s area is its own, or else its device’s.
- Its floor is the floor of that area.
- Its labels are its own, its device’s and its area’s.
- A hidden entity, or a configuration or diagnostic one, never matches an area, floor, label or device.
An entity without an entry in the entity registry, such as a YAML entity without a unique ID, has no area, floor, labels or device.
Looking things up
Section titled “Looking things up”$this->ha->getRegistry() returns Stewart’s copy of the registries. Lookups are instant.
| Method | Returns |
|---|---|
listAreas(), findArea($id), findAreaByName('Kitchen') |
Areas; a name also matches an alias, ignoring case |
listAreasOnFloor($floorId) |
The areas on one floor |
listFloors(), findFloor($id), findFloorByName(…) |
Floors |
listLabels(), findLabel($id), findLabelByName(…) |
Labels |
listDevices(), findDevice($id), listDevicesInArea($areaId) |
Devices |
listEntities(), findEntity($entityId) |
Entity registry entries |
findEntityPlacement($entityId) |
The device, area, floor and labels an entity ends up with |
To turn a display name into an ID: $this->ha->getRegistry()->findAreaByName('Kitchen')?->areaId.
When the registries change
Section titled “When the registries change”Stewart reads the registries on every connect, and again a second after Home Assistant reports a change. A running watch follows the new registries from the next state change. Moving a light into the kitchen does not change its state, so a kitchen watch notices that light the next time it changes.
Editing entity entries
Section titled “Editing entity entries”An app that asks for RegistryEditor can change any entity’s entry: name, icon, area, labels, aliases, and whether it
is hidden or disabled. This app hides battery sensors and labels them:
<?php
declare(strict_types=1);
namespace App;
use Stewart\Contracts\App;use Stewart\Contracts\Automation;use Stewart\Contracts\HaContext;use Stewart\Contracts\Registry\RegistryEditor;use Stewart\Contracts\Registry\Update\EntityRegistryUpdate;
#[Automation(id: 'hide-batteries')]final readonly class HideBatteries implements App{ public function __construct( private HaContext $ha, private RegistryEditor $registry, ) {}
public function initialize(): void { foreach ($this->ha->listStates('sensor.*_battery') as $battery) { $this->registry->updateEntity($battery->entityId, new EntityRegistryUpdate() ->withAddedLabels('battery') ->withHidden(true)); } }
public function dispose(): void {}}- Fields the update leaves out stay as they are.
withoutName(),withoutIcon()andwithoutArea()hand a field back to the integration or device. withLabels()andwithAliases()replace the whole list.withAddedLabels()andwithRemovedLabels()(and the same for aliases) keep the entries others set.updateEntity()returns the entry Home Assistant saved. Stewart’s copy catches up about a second later.- Edits count against the service call limits, and with dry run on they are logged, not sent.
It throws RegistryEditException:
| Reason | When |
|---|---|
NotFound |
The entity has no registry entry |
NothingToUpdate |
The update changes nothing |
Rejected |
Home Assistant refused it, for example enabling an entity of a disabled device |
Unreachable, TimedOut, Overloaded |
As for service calls |
Testing
Section titled “Testing”$harness->ha->registry is an in-memory registry. Seed areas, floors, labels, devices and entities, and filters
resolve against them:
<?php
declare(strict_types=1);
namespace App\Tests;
use App\RoomMotion;use PHPUnit\Framework\TestCase;use Stewart\Contracts\Entity\EntityId;use Stewart\Contracts\Registry\Area;use Stewart\Contracts\Registry\AreaId;use Stewart\Contracts\Registry\LabelId;use Stewart\Contracts\Registry\RegisteredEntity;use Stewart\Testing\App\AppHarness;
final class RoomMotionTest extends TestCase{ public function testMotionInTheAreaTurnsItsLightsOn(): void { $harness = new AppHarness(); $harness->ha->registry ->seedArea(new Area(new AreaId('kitchen'), 'Kitchen')) ->seedEntity(new RegisteredEntity(new EntityId('binary_sensor.kitchen_pir'), areaId: new AreaId('kitchen'), labelIds: [new LabelId('motion')])) ->seedEntity(new RegisteredEntity(new EntityId('binary_sensor.hall_pir'), areaId: new AreaId('hall'), labelIds: [new LabelId('motion')])); $harness->ha->seedState('binary_sensor.kitchen_pir', 'off'); $harness->ha->seedState('binary_sensor.hall_pir', 'off'); $harness->startApp(RoomMotion::class);
$harness->ha->pushState('binary_sensor.hall_pir', 'on'); self::assertTrue($harness->ha->calls->isEmpty());
$harness->ha->pushState('binary_sensor.kitchen_pir', 'on'); self::assertSame('light.turn_on', $harness->ha->getLastCall()->getServiceName()); }}Edits through $harness->registryEditor change the same in-memory registry, and its updates property lists them:
<?php
declare(strict_types=1);
namespace App\Tests;
use App\HideBatteries;use PHPUnit\Framework\TestCase;use Stewart\Contracts\Entity\EntityId;use Stewart\Contracts\Registry\RegisteredEntity;use Stewart\Testing\App\AppHarness;
final class HideBatteriesTest extends TestCase{ public function testBatterySensorsAreHiddenAndLabelled(): void { $harness = new AppHarness(); $harness->ha->registry->seedEntity(new RegisteredEntity(new EntityId('sensor.hall_battery'))); $harness->ha->seedState('sensor.hall_battery', '80');
$harness->startApp(HideBatteries::class);
$entry = $harness->ha->registry->findEntity('sensor.hall_battery'); self::assertTrue($entry?->isHidden()); self::assertSame(['battery'], array_map(strval(...), $entry->labelIds)); }}Common problems
Section titled “Common problems”The filter matches nothing. It needs IDs, not display names. Check the entity’s area under Settings → Devices & services → Entities, and whether the entity is hidden.
Floors and labels are empty. Home Assistant before 2024.4 has none.