Skip to content

Blinds against the sun

Every ten minutes, checks whether the sun stands in front of the south facade and it is hot outside. If so, it closes the blinds. At sunset it opens them again.

You need a cover for the blinds, an outdoor temperature sensor, and a location set in Home Assistant.

apps/SunBlinds.php
<?php
declare(strict_types=1);
namespace App;
use Stewart\Contracts\App;
use Stewart\Contracts\Automation;
use Stewart\Contracts\HaContext;
use Stewart\Contracts\Schedule\Scheduler;
use Stewart\Contracts\Service\ServiceTarget;
use Stewart\Contracts\Sun\SunCalendar;
use Stewart\Contracts\Sun\SunEvent;
use Stewart\Contracts\Time\Duration;
#[Automation(id: 'sun-blinds')]
final readonly class SunBlinds implements App
{
public function __construct(
private HaContext $ha,
private Scheduler $scheduler,
private SunCalendar $sun,
private string $blinds = 'cover.south_blinds',
private string $outsideTemperature = 'sensor.outside_temperature',
private float $closeAbove = 24.0,
private float $facadeFrom = 135.0,
private float $facadeTo = 225.0,
) {}
public function initialize(): void
{
$this->scheduler->runEvery(Duration::minutes(10), function (): void {
if ($this->isSunOnTheFacade() && $this->isWarmOutside() && $this->areBlindsIn('open')) {
$this->moveBlinds('close_cover');
}
});
$this->scheduler->runAtSunEvent(SunEvent::Sunset, function (): void {
if ($this->areBlindsIn('closed')) {
$this->moveBlinds('open_cover');
}
});
}
public function dispose(): void {}
private function isSunOnTheFacade(): bool
{
$position = $this->sun->getCurrentPosition();
return $position->isWithinAzimuth($this->facadeFrom, $this->facadeTo) && $position->isHigherThan(10.0);
}
private function isWarmOutside(): bool
{
return ($this->ha->getState($this->outsideTemperature)?->getStateAsFloat() ?? 0.0) > $this->closeAbove;
}
private function areBlindsIn(string $state): bool
{
return $this->ha->getState($this->blinds)?->state === $state;
}
private function moveBlinds(string $service): void
{
$this->ha->callService('cover', $service, target: ServiceTarget::forEntities($this->blinds));
}
}
apps:
sun-blinds:
options:
blinds: cover.south_blinds
outsideTemperature: sensor.outside_temperature
closeAbove: 24
facadeFrom: 135 # compass degrees the window faces, from…
facadeTo: 225 # …to
  • getCurrentPosition() gives the sun’s compass direction and height, computed from Home Assistant’s location. isWithinAzimuth() checks the direction, and isHigherThan(10.0) ignores a sun low behind the neighbours.
  • The blinds only move when they are open or closed, so the app does not fight someone who set them by hand.
  • runAtSunEvent(SunEvent::Sunset, …) opens them in the evening.
tests/SunBlindsTest.php
<?php
declare(strict_types=1);
namespace App\Tests;
use App\SunBlinds;
use PHPUnit\Framework\TestCase;
use Stewart\Contracts\Time\Duration;
use Stewart\Testing\App\AppHarness;
final class SunBlindsTest extends TestCase
{
private AppHarness $harness;
protected function setUp(): void
{
// The harness's sun stands in the south, 45° high, from 06:00 to 18:00.
$this->harness = new AppHarness();
$this->harness->ha->seedState('cover.south_blinds', 'open');
$this->harness->startApp(SunBlinds::class);
}
public function testTheBlindsCloseOnAHotSunnyDay(): void
{
$this->harness->ha->seedState('sensor.outside_temperature', '28');
$this->harness->advanceTime(Duration::hours(10));
self::assertSame('cover.close_cover', $this->harness->ha->getLastCall()->getServiceName());
}
public function testTheBlindsStayOpenOnACoolDay(): void
{
$this->harness->ha->seedState('sensor.outside_temperature', '18');
$this->harness->advanceTime(Duration::hours(10));
self::assertTrue($this->harness->ha->calls->isEmpty());
}
public function testTheBlindsOpenAtSunset(): void
{
$this->harness->ha->seedState('sensor.outside_temperature', '18');
$this->harness->ha->pushState('cover.south_blinds', 'closed');
$this->harness->advanceTime(Duration::hours(18));
self::assertSame('cover.open_cover', $this->harness->ha->getLastCall()->getServiceName());
}
}
  • Find your facade’s direction with a compass app: east is 90°, south 180°, west 270°.
  • Close them only partly: call cover.set_cover_position with a position.