1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-29 08:32:26 +01:00
Files
esphome/esphome/components/sun/__init__.py
2025-03-20 09:51:23 -10:00

182 lines
5.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import re
from esphome import automation
import esphome.codegen as cg
from esphome.components import time
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_TIME_ID,
CONF_TRIGGER_ID,
)
CODEOWNERS = ["@OttoWinter"]
sun_ns = cg.esphome_ns.namespace("sun")
Sun = sun_ns.class_("Sun")
SunTrigger = sun_ns.class_(
"SunTrigger", cg.PollingComponent, automation.Trigger.template()
)
SunCondition = sun_ns.class_("SunCondition", automation.Condition)
CONF_SUN_ID = "sun_id"
CONF_ELEVATION = "elevation"
CONF_ON_SUNRISE = "on_sunrise"
CONF_ON_SUNSET = "on_sunset"
# Default sun elevation is a bit below horizon because sunset
# means time when the entire sun disk is below the horizon
DEFAULT_ELEVATION = -0.83333
ELEVATION_MAP = {
"sunrise": 0.0,
"sunset": 0.0,
"civil": -6.0,
"nautical": -12.0,
"astronomical": -18.0,
}
def elevation(value):
if isinstance(value, str):
try:
value = ELEVATION_MAP[
cv.one_of(*ELEVATION_MAP, lower=True, space="_")(value)
]
except cv.Invalid:
pass
value = cv.angle(value)
return cv.float_range(min=-180, max=180)(value)
# Parses sexagesimal values like 22°577″S
LAT_LON_REGEX = re.compile(
r"([+\-])?\s*"
r"(?:([0-9]+)\s*°)?\s*"
r"(?:([0-9]+)\s*[\'])?\s*"
r'(?:([0-9]+)\s*[″"])?\s*'
r"([NESW])?"
)
def parse_latlon(value):
if isinstance(value, str) and value.endswith("°"):
# strip trailing degree character
value = value[:-1]
try:
return cv.float_(value)
except cv.Invalid:
pass
value = cv.string_strict(value)
m = LAT_LON_REGEX.match(value)
if m is None:
raise cv.Invalid("Invalid format for latitude/longitude")
sign = m.group(1)
deg = m.group(2)
minute = m.group(3)
second = m.group(4)
d = m.group(5)
val = float(deg or 0) + float(minute or 0) / 60 + float(second or 0) / 3600
if sign == "-":
val *= -1
if d and d in "SW":
val *= -1
return val
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(Sun),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Required(CONF_LATITUDE): cv.All(
parse_latlon, cv.float_range(min=-90, max=90)
),
cv.Required(CONF_LONGITUDE): cv.All(
parse_latlon, cv.float_range(min=-180, max=180)
),
cv.Optional(CONF_ON_SUNRISE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger),
cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation,
}
),
cv.Optional(CONF_ON_SUNSET): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger),
cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation,
}
),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time(time_))
cg.add(var.set_latitude(config[CONF_LATITUDE]))
cg.add(var.set_longitude(config[CONF_LONGITUDE]))
for conf in config.get(CONF_ON_SUNRISE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
await cg.register_component(trigger, conf)
await cg.register_parented(trigger, var)
cg.add(trigger.set_sunrise(True))
cg.add(trigger.set_elevation(conf[CONF_ELEVATION]))
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_SUNSET, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
await cg.register_component(trigger, conf)
await cg.register_parented(trigger, var)
cg.add(trigger.set_sunrise(False))
cg.add(trigger.set_elevation(conf[CONF_ELEVATION]))
await automation.build_automation(trigger, [], conf)
@automation.register_condition(
"sun.is_above_horizon",
SunCondition,
cv.Schema(
{
cv.GenerateID(): cv.use_id(Sun),
cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable(
elevation
),
}
),
)
async def sun_above_horizon_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double)
cg.add(var.set_elevation(templ))
cg.add(var.set_above(True))
return var
@automation.register_condition(
"sun.is_below_horizon",
SunCondition,
cv.Schema(
{
cv.GenerateID(): cv.use_id(Sun),
cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable(
elevation
),
}
),
)
async def sun_below_horizon_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
templ = await cg.templatable(config[CONF_ELEVATION], args, cg.double)
cg.add(var.set_elevation(templ))
cg.add(var.set_above(False))
return var