Skip to content

Heating by presence

Sets the thermostats to away once everyone has been gone for 15 minutes, and back to home as soon as someone returns. A quick trip to the shop changes nothing.

You need a group or person that is home or not_home (such as a group of person entities), and thermostats with away and home presets.

apps/PresenceHeating.php
<?php
declare(strict_types=1);
namespace App;
use Stewart\Contracts\App;
use Stewart\Contracts\Automation;
use Stewart\Contracts\HaContext;
use Stewart\Contracts\Service\ServiceTarget;
use Stewart\Contracts\Time\Duration;
#[Automation(id: 'presence-heating')]
final readonly class PresenceHeating implements App
{
/** @param list<string> $thermostats */
public function __construct(
private HaContext $ha,
private string $household = 'group.family',
private array $thermostats = ['climate.living_room', 'climate.bedroom'],
private int $awayAfterMinutes = 15,
) {}
public function initialize(): void
{
$this->ha->watchStateChanges($this->household)
->whenChangedTo('not_home', for: Duration::minutes($this->awayAfterMinutes))
->subscribe(fn() => $this->setPreset('away'));
$this->ha->watchStateChanges($this->household)
->whenChangedTo('home', from: 'not_home')
->subscribe(fn() => $this->setPreset('home'));
}
public function dispose(): void {}
private function setPreset(string $preset): void
{
$this->ha->callService('climate', 'set_preset_mode', ['preset_mode' => $preset], ServiceTarget::forEntities(...$this->thermostats));
}
}
apps:
presence-heating:
options:
household: group.family
thermostats: [climate.living_room, climate.bedroom, climate.office]
awayAfterMinutes: 15
  • whenChangedTo('not_home', for: …) fires only after the household has been away for the whole time.
  • whenChangedTo('home', from: 'not_home') fires on the way back, but not when Home Assistant restarts and the group goes from unknown to home.
  • One service call targets every thermostat at once.
tests/PresenceHeatingTest.php
<?php
declare(strict_types=1);
namespace App\Tests;
use App\PresenceHeating;
use PHPUnit\Framework\TestCase;
use Stewart\Contracts\Time\Duration;
use Stewart\Testing\App\AppHarness;
final class PresenceHeatingTest extends TestCase
{
private AppHarness $harness;
protected function setUp(): void
{
$this->harness = new AppHarness();
$this->harness->ha->seedState('group.family', 'home');
$this->harness->startApp(PresenceHeating::class);
}
public function testTheHeatingGoesToAwayOnceEveryoneHasLeft(): void
{
$this->harness->ha->pushState('group.family', 'not_home');
$this->harness->advanceTime(Duration::minutes(15));
$call = $this->harness->ha->getLastCall();
self::assertSame(['preset_mode' => 'away'], $call->data);
self::assertSame(['climate.living_room', 'climate.bedroom'], $call->listTargetedEntityIds());
}
public function testAQuickTripToTheShopChangesNothing(): void
{
$this->harness->ha->pushState('group.family', 'not_home');
$this->harness->advanceTime(Duration::minutes(10));
$this->harness->ha->pushState('group.family', 'home');
self::assertSame(['preset_mode' => 'home'], $this->harness->ha->getLastCall()->data);
self::assertCount(1, $this->harness->ha->calls);
}
}
  • Thermostats without presets: call climate.set_temperature with a temperature instead.
  • Heat up before you arrive: use a zone trigger for “entering a zone around home”.