Ramp light levels based on twilight and mode



Following up on my Astronomical Data API post…

This is the piston I use to set the light levels throughout the house automatically adjusted for twilight and mode. The levels change according to actual twilight, not just a set timeframe to sunrise/sunset. That time it takes to go from day to night or night to day changes throughout the year.

It automatically calculates the number of steps and seconds between the steps to smoothly brighten or darken the lights based on the astronomical data (time of year).

All of my in-wall switches are from Inovelli. One of the things that I like about them is that you can set the brightness level while they are off. This piston sets the level for when they are next physically pressed. By using their child device here, this piston doesn’t change the brightness of the light that’s already on, it changes the brightness of what will be the next time it’s pressed.

It also sets the level for when we turn in for the night. Most of these devices are the bathroom lights, shower lights, etc.

In the evening, the piston starts to set the levels from my daytime setting (90% full) down to 40% full. By the time dusk settles in, they are at 40% which is plenty bright for the evening. In the morning, they ramp up the other way so they are pack at my daytime level by the time the sun rises.

The other neat thing here is when we change ST over to night mode, they are all set to 15% brightness so no one gets blinded when they turn on the bathroom lights in the middle of the night!

We don’t have a door between the bathroom and bedroom in our master suite. Now, when either of us is in bed, we don’t get blinded when the other turns on the light over our master vanity. It starts at 15% brightness instead of 100%!

It all works seamlessly and really works well. If you have color changing LEDs, you could use this same logic to adjust the color temperature based on the circadian time stamps from the global API.

Astronomical Data API

To help keep organized, here is the link to the API piston.


Nice set of pistons, @MHedish

I feel compelled to make one observation… If your location mode does not change at all between (Sunrise-3hrs) and {@astronomical_sunrise}, then your early morning trigger will always fire at yesterdays time. Not a huge deal really, since there is only about 1 or 2 minutes difference each day. I just though it worth mentioning.

The reason this happens is because once a wakeup has been scheduled, it will not see any changes to the global until after the piston runs next.

The solution is to make sure the piston runs (at least) once after the global has been changed, but before the real trigger. When that happens, this piston will see the new global time, and schedule a proper wakeup.

Adding this simple block to the piston above will solve this:

Every day at 175 minutes before sunrise

That “Do” is intentionally left blank. It’s only purpose is to refresh the wakeup time based on the new data.

(I chose “175 min before sunrise” because your other piston updates the global variables 5 min earlier)


Alternatively, you could go with:

Every day at 10 minutes before @astronomical_sunrise

Pro Tip:

If you do not ensure that this piston updates the wakeup schedule some time between the global updating and at least 5-10 min before the {@astronomical_sunrise} happens, then half the year, it will actually fire twice in the mornings. (the first one will be yesterdays time… then it will see the correct time, and schedule another wakeup in 1 or 2 minutes) The other half of the year, it will only fire once, at yesterday’s time. (this logic depends on whether the sunrise is getting later or earlier each day)

To embellish a bit:
Depending on your latitude, in the first half of January, your sunrise times should start inching backwards (a bit earlier each day), so only one trigger will execute. Similarly, in the first half of June, the sunrise starts inching forwards (a bit later each day), so you might see double triggers after that.

To summarize, all this old data and double triggers can be resolved by making sure that it always runs at least once sometime between (Sunrise-175min) and {@astronomical_sunrise-10min}.
(you may be able to push those offsets 4 minutes farther, but I wouldn’t)



It makes sense that the pistons “resubscribe” to event timers once they run and not based on an external variable changing value. :wink:

I like the alternative method best. Have the piston reset the timers 30 minutes before they would fire for the day.

It would be nice if there were a way to call the child pistons and have them reset their subscriptions from the master piston. That way it would force the subscriptions based on success/failure of the API call rather than presuming it worked.


The API piston can call this piston… (either by External URL or execute piston)

The one tricky part though is…

A global is not written until the last line of code has executed.

This means, If the API piston is writing to the globals, and calling this piston… then this piston will still see the old data.

In all my thousands of pistons, I have seen only one exception to this rule.
(I would not rely on it)


I tried calling it via execute piston and while the piston did run successfully, it didn’t update the timers after exiting. Variables (local and global) update upon exit just fine.

Even with an empty “Do” loop (and a Do NoOperation) I couldn’t get the timers to reset. It just ran through once and exited as it should.

I’m open to suggestions but from what I’ve seen so far, a timer needs to execute before the piston will update its own timers on exit. Your idea to use a timer with a do nothing operation before the first scheduled run of the day works great.


I had to do something similar with the Moon’s rise & set times.
(They change so drastically day by day)

At least, with your piston above, even with an error, you should still be within about two minutes of accuracy. Mine was a real challenge because one false reading meant hours of inaccuracy. :-1:

(Well, that plus I wanted the data and tile image to update shortly after the Moon rose or set)

In the end, my “Do nothing” portion actually “Sets piston state”.
(this is just so I can see a status update on my Dashboard)


You might get different results by calling it via the External URL method.

Alternatively, there is also the possibility that at least one line of code has to be processed before the times will update. (Logs set to “Full” might confirm this)

Personally? I usually have a small WAIT in the receiving piston…

Unfortunately, I don’t have the freetime at the moment to test this furthur.

My vote is still with:

Every day at 10 minutes before @astronomical_sunrise



I found a way to have the Astronomical API update the timers for the child pistons with the updated global datetime variables automatically. No need for a do-nothing loop in the children nor having to execute them prior to their first scheduled run of the day (e.g. 10 minutes before @astronomical_sunrise).

My manual test have worked perfectly, including watching the pistons all update their timers on the dashboard at once. I want to let it run for a day or so just to be sure no interaction with the children themselves messes it up.

BTW – Executing via the external URL method didn’t make a difference, so I kept hunting for a way.

As soon as I’m sure it will work, I’ll post the green copy. :slight_smile:


Please do. This would help many of us.


This one took me a bit longer than I had originally thought. Lots of trial and error.

Having the child pistons resubscribe their events and triggers once the JSON call updated the global variables was relatively straightforward. I’m using the Resume piston command to restart them individually. The parent piston simply updates the globals and then, effectively, restarts the child pistons.

The struggle was with having the API piston update itself. That proved to be an exercise in futility. I couldn’t find a way to have the parent resubscribe and restarting itself wasn’t working either. I separated the JSON API call in the Astronomical Data API from the code that updated the boolean flags for twilight periods (the synodic events). By making that a separate child piston, it can be restarted and the only event that remains in the parent piston is the one that doesn’t need to be as precise (the API call itself which is subscribed to the built-in sunset event.

I also found that I had to wait for the global variables to update before starting the Resume piston processes. The first piston called didn’t update to the new values but the following pistons did. Once I inserted the 10 second wait, everything works as desired.

Here are the two new pistons:

Astronomical Data API: tybg

Astronomical Data Synodic: e8kiz

I also made some new additions since posting the original piston. The tiles now show the times, length of day, moonrise / moonset / phase and current synodic period. The piston state has the last time and status of the API call just in case we need to do any troubleshooting.


Why? Because we can. :wink:

Astronomical Data API

Oh… I thought you discovered a new method. Resuming a piston does the exact same thing as my earlier statement:

Pausing and resuming a piston also re-checks the globals… but for many reasons, it is very rare for me to recommend that method.

(IE: If PistonA has to pause/resume PistonB, it is usually more efficient for PistonA to simply send a command to PistonB to refresh the globals seen)


Is there a installation guide?
I installed 3 pistons.

  1. the lighs (xyau)
  2. **Astronomical Data API (tybg)
  3. Astronomical Data Synodic (e8kiz)

A. added the api url
B. changed Lat / Long
C. Created global variables for all variables in the middle of the Astronomical Data API

I’m getting in Astronomical Data Synodic:

This piston does not subscribe to any events. Unless executed by other means, it will never run on its own.

I dont see any tiles.
Any hints?


I wish there were a way to programmatically create global variables. If that were the case, I could put together something that would be more of an “install.”

I’d load tybg and then e8kiz in that order and then any other pistons (or update your existing pistons to use the new globals as triggers.

Can you upload a screenshot of the globals so I can take a look?


Is it possible you created them as the wrong type and/or different case as what is in two pistons? The globals are case-sensitive. I can see how that would give you the issue.


Hoping for some help understanding why the piston is not scheduling at the appropriate time.

It appears that all global variables have set correctly. I ran a test on the light piston so that it would re-subscribe to the globals. Next Scheduled is saying today at 10:30pm which does not correlate with any of my globals.

I did not adjust the original piston very much.


Any advice would be appreciated.


That’s really odd. Not sure what’s happening there. I just loaded your green piston, didn’t change a single thing and it subscribed correctly.

Your global variables are correct. It doesn’t appear to me to be any of the changes to the piston. It looks like a webCoRE subscription issue.

Stupid silly question… have you cleared your cache and/or tried to re-import the piston?


I would ignore this. It is likely a remnant from an earlier edit…

It is probably harmless, and will only be seen once. Let us know if the 3 real triggers fail.


If that remnant bothers you, I believe that you can pause the piston… Wait ten seconds… and then resume, to clear it up.


Thanks @WCmore and @MHedish.

Slightly OCD here so yes the remnants bugged the crap out of me :smile: Pause and resume did the trick, scheduled for sunset. Should have thought of that in the beginning. Thanks Again.