Re-write of values calculation

The basis of Circadian Lighting is to figure out how to adjust lights to mimic the sun, which is roughly parabolic above and below the horizon. Originally I used three points to calculate the parabolas - sunrise, sunset, and solar noon/midnight. I did a bunch of generic arithmetic to generate the parabola from those three points. The problem is that this arithmetic generates a parabola from _any_ three points - in reality the points I have are the intercepts and vertex.

The problem with the old method is that if solar noon/midnight did not fall exactly between sunrise and sunset then the calculated vertex could be more or less than 100%, which would cause the integration to set color/brightness values above or below the set min/max. Generating a parabola from the intercepts and vertex is much simpler and also should guarantee the min/max are what's expected. 

In addition to this change, I'm also now calculating 4 "half" parabolas instead of just one for above horizon and one for below horizon. The "half" parabolas are: sunrise->solar-noon, solar-noon->sunset, sunset->solar-midnight, solar-midnight->sunrise. This is necessary for the case where solar noon/midnight don't fall exactly half way between sunrise and sunset, which is especially prevalent for users who specify a specific time for sunrise or sunset but not both.

I'm embarrassed at how it's been before it dawned on me that I could calculate the parabolas from intercepts/vertex rather than the three random points method, but hopefully this fixes inconsistencies for users - it should definitely fix issue #25!
This commit is contained in:
Clayton Nummer 2019-06-01 14:20:47 -04:00
commit 4f332a514c
2 changed files with 65 additions and 64 deletions

View file

@ -48,7 +48,7 @@ from homeassistant.util.dt import utcnow as dt_utcnow, as_local
from datetime import datetime, timedelta
VERSION = '1.0.4'
VERSION = '1.0.5'
_LOGGER = logging.getLogger(__name__)
@ -199,77 +199,78 @@ class CircadianLighting(object):
def calc_percent(self):
utcnow = dt_utcnow()
now = as_local(utcnow)
_LOGGER.debug("now: " + str(now))
today_sun_times = self.get_sunrise_sunset(now)
_LOGGER.debug("today_sun_times: " + str(today_sun_times))
# Convert everything to epoch timestamps for easy calculation
now_seconds = now.timestamp()
today_sunrise_seconds = today_sun_times[SUN_EVENT_SUNRISE].timestamp()
today_sunset_seconds = today_sun_times[SUN_EVENT_SUNSET].timestamp()
today_solar_noon_seconds = today_sun_times['solar_noon'].timestamp()
today_solar_midnight_seconds = today_sun_times['solar_midnight'].timestamp()
sunrise_seconds = today_sun_times[SUN_EVENT_SUNRISE].timestamp()
sunset_seconds = today_sun_times[SUN_EVENT_SUNSET].timestamp()
solar_noon_seconds = today_sun_times['solar_noon'].timestamp()
solar_midnight_seconds = today_sun_times['solar_midnight'].timestamp()
_LOGGER.debug("now: " + str(now) + "\n\n today_sun_times: " + str(today_sun_times))
if now < today_sun_times[SUN_EVENT_SUNRISE]:
if now < today_sun_times[SUN_EVENT_SUNRISE]: # It's before sunrise (after midnight)
# Because it's before sunrise (and after midnight) sunset must have happend yesterday
yesterday_sun_times = self.get_sunrise_sunset(now - timedelta(days=1))
yesterday_sunrise_seconds = yesterday_sun_times[SUN_EVENT_SUNRISE].timestamp()
yesterday_sunset_seconds = yesterday_sun_times[SUN_EVENT_SUNSET].timestamp()
yesterday_solar_midnight_seconds = yesterday_sun_times['solar_midnight'].timestamp()
_LOGGER.debug("yesterday_sun_times: " + str(yesterday_sun_times))
x1 = yesterday_sunset_seconds
y1 = 0
if today_sun_times['solar_midnight'] > yesterday_sun_times[SUN_EVENT_SUNSET] and today_sun_times['solar_midnight'] < today_sun_times[SUN_EVENT_SUNRISE]:
x2 = today_solar_midnight_seconds
else:
x2 = yesterday_solar_midnight_seconds
y2 = -100
x3 = today_sunrise_seconds
y3 = 0
elif now > today_sun_times[SUN_EVENT_SUNSET]:
sunset_seconds = yesterday_sun_times[SUN_EVENT_SUNSET].timestamp()
if today_sun_times['solar_midnight'] > today_sun_times[SUN_EVENT_SUNSET] and yesterday_sun_times['solar_midnight'] > yesterday_sun_times[SUN_EVENT_SUNSET]:
# Solar midnight is after sunset so use yesterdays's time
solar_midnight_seconds = yesterday_sun_times['solar_midnight'].timestamp()
elif now > today_sun_times[SUN_EVENT_SUNSET]: # It's after sunset (before midnight)
# Because it's after sunset (and before midnight) sunrise should happen tomorrow
tomorrow_sun_times = self.get_sunrise_sunset(now + timedelta(days=1))
tomorrow_sunrise_seconds = tomorrow_sun_times[SUN_EVENT_SUNRISE].timestamp()
tomorrow_sunset_seconds = tomorrow_sun_times[SUN_EVENT_SUNSET].timestamp()
tomorrow_solar_midnight_seconds = tomorrow_sun_times['solar_midnight'].timestamp()
_LOGGER.debug("tomorrow_sun_times: " + str(tomorrow_sun_times))
sunrise_seconds = tomorrow_sun_times[SUN_EVENT_SUNRISE].timestamp()
if today_sun_times['solar_midnight'] < today_sun_times[SUN_EVENT_SUNRISE] and tomorrow_sun_times['solar_midnight'] < tomorrow_sun_times[SUN_EVENT_SUNRISE]:
# Solar midnight is before sunrise so use tomorrow's time
solar_midnight_seconds = tomorrow_sun_times['solar_midnight'].timestamp()
x1 = today_sunset_seconds
y1 = 0
_LOGGER.debug("now_seconds: " + str(now_seconds))
_LOGGER.debug("sunrise_seconds: " + str(sunrise_seconds))
_LOGGER.debug("sunset_seconds: " + str(sunset_seconds))
_LOGGER.debug("solar_midnight_seconds: " + str(solar_midnight_seconds))
_LOGGER.debug("solar_noon_seconds: " + str(solar_noon_seconds))
if today_sun_times['solar_midnight'] > today_sun_times[SUN_EVENT_SUNSET] and today_sun_times['solar_midnight'] < tomorrow_sun_times[SUN_EVENT_SUNRISE]:
x2 = today_solar_midnight_seconds
# Figure out where we are in time so we know which half of the parabola to calculate
# We're generating a different sunset-sunrise parabola for before and after solar midnight
# because it might not be half way between sunrise and sunset
# We're also (obviously) generating a different parabola for sunrise-sunset
# sunrise-sunset parabola
if now_seconds > sunrise_seconds and now_seconds < sunset_seconds:
h = solar_noon_seconds
k = 100
# parabola before solar_noon
if now_seconds < solar_noon_seconds:
x = sunrise_seconds
# parabola after solar_noon
else:
x2 = tomorrow_solar_midnight_seconds
y2 = -100
x = sunset_seconds
y = 0
x3 = tomorrow_sunrise_seconds
y3 = 0
else:
x1 = today_sunrise_seconds
y1 = 0
x2 = today_solar_noon_seconds
y2 = 100
x3 = today_sunset_seconds
y3 = 0
# sunset_sunrise parabola
elif now_seconds > sunset_seconds and now_seconds < sunrise_seconds:
h = solar_midnight_seconds
k = -100
# parabola before solar_midnight
if now_seconds < solar_midnight_seconds:
x = sunset_seconds
# parabola after solar_midnight
else:
x = sunrise_seconds
y = 0
_LOGGER.debug("x1: " + str(x1) + "\n\n y1: " + str(y1) + "\n\n x2: " + str(x2) + "\n\n y2: " + str(y2))
# Generate color temperature parabola from points
a1 = -x1**2+x2**2
b1 = -x1+x2
d1 = -y1+y2
a2 = -x2**2+x3**2
b2 = -x2+x3
d2 = -y2+y3
bm = -(b2/b1)
a3 = bm*a1+a2
d3 = bm*d1+d2
a = d3/a3
b = (d1-a1*a)/b1
c = y1-a*x1**2-b*x1
percentage = a*now_seconds**2+b*now_seconds+c
a = (y-k)/(h-x)**2
percentage = a*(now_seconds-h)**2+k
_LOGGER.debug("h: " + str(h))
_LOGGER.debug("k: " + str(k))
_LOGGER.debug("x: " + str(x))
_LOGGER.debug("y: " + str(y))
_LOGGER.debug("a: " + str(a))
_LOGGER.debug("percentage: " + str(percentage))
return percentage

View file

@ -1,7 +1,7 @@
{
"circadian_lighting": {
"updated_at": "2019-05-09",
"version": "1.0.4",
"updated_at": "2019-06-01",
"version": "1.0.5",
"local_location": "/custom_components/circadian_lighting/__init__.py",
"remote_location": "https://raw.githubusercontent.com/claytonjn/hass-circadian_lighting/master/custom_components/circadian_lighting/__init__.py",
"visit_repo": "https://github.com/claytonjn/hass-circadian_lighting",