The Ephemeris
Where is the sun right now? Not from a weather API—calculated from orbital mechanics.
Why Calculate?
Weather APIs give you sunrise/sunset times—but that's not enough. We need the sun's exact position (azimuth and altitude) at any moment to know which windows have glare.
def sun_position(lat, lon, dt=None):
"""Calculate sun azimuth and altitude."""
jd = julian_date(dt)
n = jd - 2451545.0 # Days since J2000
# Solar mean longitude
L = (280.460 + 0.9856474 * n) % 360
# Mean anomaly
g = (357.528 + 0.9856003 * n) % 360
# ... (orbital calculations)
return SunPosition(
azimuth=azimuth, # 0-360° from N
altitude=altitude, # -90° to 90°
is_day=altitude > 0
)
Window Geometry
A house with a view. 11 shades. 4 cardinal orientations. Which windows get sun when?
The Intensity Function
Not all sun exposure is equal. We calculate glare intensity based on how directly the sun hits each window and the sun's altitude.
def get_sun_intensity(sun_az, sun_alt, direction):
"""0.0-1.0 glare intensity."""
if sun_alt <= 0: return 0.0
window_az = AZIMUTH[direction]
diff = abs(sun_az - window_az)
if diff > 180: diff = 360 - diff
# Sun within 60° acceptance angle?
if diff > 60: return 0.0
alignment = 1.0 - (diff / 60.0)
# Lower sun = more glare
if sun_alt < 15:
altitude_factor = 1.0
elif sun_alt < 45:
altitude_factor = 1.0 - ((sun_alt - 15) / 45)
else:
altitude_factor = max(0, 0.33 - ...)
return alignment * altitude_factor
Direction Constants
| Direction | Azimuth | What Faces It | Sun Times |
|---|---|---|---|
| ● SOUTH | 180° | Living, Dining, Entry | 10 AM – 3 PM |
| ● EAST | 90° | Living Room side | 6 AM – 11 AM |
| ● WEST | 270° | Primary Bed, Bed 4 | 3 PM – 8 PM |
| ● NORTH | 0° | Primary Bath | Never direct |
Weather Integration
Celestial calculations assume clear skies. Clouds change everything.
The Override Logic
When it's cloudy or raining, there's no glare to block. Weather data from OpenWeatherMap One Call API 3.0 can override celestial calculations.
async def optimize_shades_celestial(self):
# Get current weather
weather = await self._weather.get_current()
# Weather override?
if weather.cloud_coverage > 70:
return open_all_shades() # No glare
if weather.condition in ['rain', 'thunderstorm']:
return open_all_shades() # Enjoy the rain
# Normal celestial optimization
sun = sun_position(HOME_LAT, HOME_LON)
for shade in SHADES:
level = calculate_shade_level(
shade, sun.azimuth, sun.altitude, sun.is_day
)
await self.set_shade(shade.id, level)
Weather Conditions
| Condition | Action | Why |
|---|---|---|
| ☀️ Clear | Celestial optimization | Full sun exposure |
| ⛅ Partly Cloudy | Celestial optimization | Still significant glare |
| ☁️ Cloudy (70%+) | 100% OPEN | Diffused light, no glare |
| 🌧️ Rain | 100% OPEN | Enjoy watching it rain |
| ⛈️ Thunderstorm | 100% OPEN | Nature's show |
Celestial Triggers
Event-driven automation. The system doesn't poll—it watches for astronomical events.
Morning Optimization
East-facing windows get adjusted first. Living Room East (237) may close to 60% as morning sun streams in. Other shades stay open.
Peak Sun
South-facing windows see maximum exposure. Living South (235), Dining South (243), Entry (229) adjust based on altitude. High sun = less glare.
West Exposure
Sun moves west. Primary West (68) and Bed 4 shades (359, 361) begin adjusting. Late afternoon sun is low and intense—maximum glare potential.
Evening Opening
Sun drops below horizon. All shades open to 100%. Enjoy the evening light. Prepare for sunset views.
ResidentOverrideCBF (Control Barrier Function) protects your explicit intent for 30 minutes. Your judgment matters more than my algorithm.
Live Demo
Interactive simulation. Drag the time slider to see how shade recommendations change throughout the day.
Sun Position
Shade Recommendations
The Architecture
How it all fits together. From orbital mechanics to motor commands—this is where astronomy meets home comfort.
┌─────────────────────────────────────────────────────────────────┐ │ CELESTIAL TRIGGER ENGINE │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ EPHEMERIS │ │ HOME GEOM │ │ WEATHER │ │ │ │ │ │ │ │ │ │ │ │ sun_position │───▶│ get_sun_ │ │ cloud_cover │ │ │ │ (lat, lon, │ │ intensity() │◀───│ condition │ │ │ │ datetime) │ │ │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ │ │ ▼ │ │ │ ┌──────────────────┐ │ │ │ │ calculate_shade │◀─────────┘ │ │ │ _level() │ │ │ └────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ RESIDENT OVERRIDE CBF │ │ │ │ h(x) ≥ 0 → Manual overrides protected │ │ │ └─────────────────────────────┬────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ CONTROL4 / LUTRON │ │ │ │ 11 Motorized Shades · 4 Directions │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
h(x) ≥ 0. If you manually adjust a shade, the system won't override you. Your explicit intent is protected for 30 minutes.
config/location.yaml or environment variables. Move houses? Just update the config. The algorithms don't change.