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.
<?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 # …toHow it works
Section titled “How it works”getCurrentPosition()gives the sun’s compass direction and height, computed from Home Assistant’s location.isWithinAzimuth()checks the direction, andisHigherThan(10.0)ignores a sun low behind the neighbours.- The blinds only move when they are
openorclosed, so the app does not fight someone who set them by hand. runAtSunEvent(SunEvent::Sunset, …)opens them in the evening.
<?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()); }}Make it yours
Section titled “Make it yours”- Find your facade’s direction with a compass app: east is 90°, south 180°, west 270°.
- Close them only partly: call
cover.set_cover_positionwith aposition.