Reverse Suncalc calculation

In the reverse of @augoisms Suncalc driver, I need to calculate each day when the sun is at a particular Azimuth or Altitude (ascending or descending).

Use case:
Many of the shade movements are based on time of day. It's only when the sun shines directly in the window that I need to adjust the shades.
If I could calculate the time of different sun positions I could save them in Hub Variables and use Schedule Manager for these times.
Already using Schedule Manager for Sunrise, Sunset and some other times. This is great to get all shades in one child app.

Anyone interested?

So you are referring to this:

I'm just wondering how are you going to calculate the input values for Azimuth or Altitude. First you would need the raw values, so I don't think even if sunCalc was reversed, you would have the data to input without measuring it from your window. You can get those values from sunCalc, so you don't want to actually reverse it, you just want to know when it happens as an event.

You really just need to know when sunCalc is reporting values within your parameters (since you would need to account for changing Azimuth and Altitude, every day. You are trying to schedule a timed event to happen, when there are already hub events being sent from sunCalc every time it calculates. That like setting up a rule that sets the sunrise time in a hub variable every day at midnight, so that scheduler can use it to trigger a sunrise action. You already have the live event in sunCalc, so this should be an event trigger, not a schedule.

You need a rule trigger that says when Altitude is between such and such, and Azimuth is between such and such (based on the same values you would use for inputs if sunCalc was reversed), then trigger your rule. It would need to use a range with a precision that will not trigger twice, or use a boolean to keep track of when it has already triggered for the day.

I checked, and it is about a 15 degrees per hour change for both altitude and azimuth. So that is 1 degree every four minutes. How accurate were you looking to get with your inputs to a possible reversed sunCalc to determine a scheduling time? If you miss altitude by one degree, it will cause a four minute error in your schedule, and so on.

I don't think that there is a need to schedule another event, when you have event data being sent as it happens from sunCalc that you just need to filter into a trigger.

I would be interested to see what you have. I used AI to create a driver to find peak window sun time, and some offsets to use as triggers for rules. It sorta works, but I am sure it could be cleaner. I hesitate to publish it because being AI, it probably ripped off everyone on this forums code and there is no way to properly attribute anyone that the AI stole code from.

I know the altitudes and azimuths that I want to check.
My use case is to calculate the times once a day and use Schedule Manager to set the shades. It is cleaner and easier to see all of those times in Schedule Manager for all 9 shades than to have the same thing in the Shade Height rule for each shade. These rules keep the sunlight within a certain distance of the window.
Already have several time variables that are set daily just after midnight. This would add 6 more. 2 each for East, South and West windows.
Additional bonus is that I can use those times to start and stop the Suncalc and Shade Height rules.

My point was it is a coordinate system that changes daily. You can't just use one set of Altitude and Azimuth values every day, as they will not be the same every day. Maybe you could isolate just Altitude, if that is all that matters, as every day should have a matching altitude, but every day the Azimuth would be different when the sun is at that altitude. I think what you would find is there will be no time of day tomorrow for your Azimuth and Altitude settings that are valid today. The sun changes both values daily, how were you planning to match both values?

You wouldn't just get a time, you would get a date and time for your values. It would always report the the actual day and time those values match, so those values will give you the same exact day and time every day - For the day and a time of the year when those values match. I imagine you would find that there is one day in the fall, and one day in the spring, where values match your settings, but all other days would find no match.

Since the sun repeats values twice a year, I'm not even sure what kind of output you would expect, it would have to be able to calculate the two dates and times for when your values are valid before and after solstice.

The input to sunCalc is current DATE and TIME. Not just time. So any values you enter into a reversed sunCalc would output to you the DATES and TIMES per year where your values match the sun. That doesn't help you for daily scheduling.

Edit: From your previous thread, I still suggest you just use light sensors. Real time data is always going to be better than a calculation. A sensor will also take into account cloudy days, as you can use a threshold for the illuminance to see if you even need to close the shade, since any calculated values will be oblivious to the actual sun/cloud conditions which really what you are basing the shade level on.

If you are unsure how to use the altitudes and azimuths from the suncalc driver in RM, i have an example in a previous post.

I can appreciate this logic, I try to do this as well to best of my ability… but sometimes is better to break things out into separate child apps to keep the automations simpler. Both approaches have their advantages/disadvantages and sometimes it takes a bit of trial and error to find the best way that works for you.

I think you should publish it with proper attributions. What calculations did you implement? I was doing something similar starting out with some dawn/dusk calculations. Both ChatGPT and Grok suck at formulaic precision in my experience, failing time after time to copy formulas from suncalc.js to the Hubitat driver. I imagine part of the issue is my prompting. I did get it to add Solar Incidence Angle

I'm currently using Suncalc for my Shade Height rule. It works great to give the current altitude and azimuth.
What I need is a time calculation for when today the Sun will be at a specific azimuth or altitude (ascending or descending)
This is the inverse of the Suncalc calculation.

Since your responses show that you haven't understood my request, please stop telling me that I shouldn't do it.

I do understand your request. I'm trying to point out to you that the reversal will not work how think it will, and I doubt that will be done by the author anyway. You will only get specific date and time, not a "daily time" if you plug in the same values every day to a reverse calculation.

You should have posted this on the sunCalc release post and have the author of the code answer you. You still should do that, they would be the best bet for someone to reverse the code for you.

" While the sun's position changes throughout the year, there are specific days when the sun's altitude and azimuth values will be the same as on another day of the year:

  • Two days a year: For any given location and time of day, there are typically two specific days in the year when the Sun will have the same altitude and azimuth values. This is because the sun's path across the sky changes gradually throughout the year, creating a figure-eight pattern known as the analemma when its position is plotted at the same time each day. The intersection points of this figure-eight represent the days when the altitude and azimuth values are the same."

last response
I know that the position changes daily. my goal is to calculate the time for a specific azimuth, with whatever the altitude, for that specific day. The same for a specific altitude, with whatever azimuth, for that day.
That is why I said that you didn't understand my request.
btw, you'll find that the Suncalc driver post is closed

Good enough. That is first time you explained you are going to use one or the other but not both together, so no, you didn't explain it, so how could anyone, including me, understand what you were trying to do?

Try using AI to build your driver. This is what I got with "Create a virtual driver for hubitat, that will take an input of a date and a sun altitude value in a command, and output the time of day the sun will be at that altitude, set in an attribute" Then repeat to get a driver for Azimuth.

metadata {
    definition(name: "Sun Altitude Calculator", namespace: "yourNamespace", author: "Your Name") {
        capability "Initialize"
        
        command "calculateSunAltitudeTime", ["date", "number"]
        
        attribute "sunAltitudeTime", "string"
        attribute "lastCalculation", "string"
        attribute "targetAltitude", "number"
    }
    
    preferences {
        input name: "latitude", type: "number", title: "Latitude", description: "Your location's latitude", required: true, defaultValue: 0
        input name: "longitude", type: "number", title: "Longitude", description: "Your location's longitude", required: true, defaultValue: 0
        input name: "timeZone", type: "text", title: "Time Zone", description: "Your time zone (e.g., America/New_York)", required: true
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
    }
}

def installed() {
    log.info "Sun Altitude Calculator installed"
    initialize()
}

def updated() {
    log.info "Sun Altitude Calculator updated"
    initialize()
}

def initialize() {
    log.info "Sun Altitude Calculator initialized"
    if (logEnable) runIn(1800, logsOff)
}

def logsOff() {
    log.warn "Debug logging disabled"
    device.updateSetting("logEnable", [value:"false", type:"bool"])
}

def calculateSunAltitudeTime(date, altitude) {
    if (logEnable) log.debug "Calculating sun altitude time for date: ${date}, altitude: ${altitude}°"
    
    try {
        // Validate inputs
        if (!latitude || !longitude) {
            log.error "Latitude and longitude must be set in preferences"
            return
        }
        
        def targetDate = Date.parse("yyyy-MM-dd", date)
        def targetAlt = altitude.toDouble()
        
        // Set attributes for tracking
        sendEvent(name: "targetAltitude", value: targetAlt)
        sendEvent(name: "lastCalculation", value: new Date().format("yyyy-MM-dd HH:mm:ss", location.timeZone))
        
        // Calculate sunrise/sunset times for reference
        def location = new Date().location
        def sunTimes = getSunriseAndSunset(date: targetDate, location: location)
        
        // This is where the actual calculation would happen
        // Note: This is a simplified approximation - a real implementation would need
        // proper astronomical calculations or a library like JSUNTIME
        
        // For demonstration, we'll use a simple approximation:
        // Assume solar noon is halfway between sunrise and sunset
        // And altitude changes linearly (which isn't accurate, but works for demo)
        
        def sunrise = sunTimes.sunrise
        def sunset = sunTimes.sunset
        def solarNoon = new Date((sunrise.time + sunset.time) / 2)
        
        // Max altitude at solar noon (simplified)
        def maxAltitude = 90 - Math.abs(latitude - targetDate.declination())
        
        if (targetAlt > maxAltitude) {
            log.warn "Requested altitude ${targetAlt}° is higher than maximum sun altitude ${maxAltitude}° for this date"
            sendEvent(name: "sunAltitudeTime", value: "Never (too high)")
            return
        }
        
        // Calculate approximate times (morning and afternoon)
        // This is a very rough approximation - real implementation would need proper math
        def fraction = targetAlt / maxAltitude
        def timeOffset = (solarNoon.time - sunrise.time) * fraction
        
        def morningTime = new Date(sunrise.time + timeOffset)
        def afternoonTime = new Date(solarNoon.time + (solarNoon.time - morningTime.time))
        
        def result = "${morningTime.format("HH:mm", location.timeZone)} or ${afternoonTime.format("HH:mm", location.timeZone)}"
        
        sendEvent(name: "sunAltitudeTime", value: result)
        if (logEnable) log.debug "Calculated sun altitude time: ${result}"
        
    } catch (Exception e) {
        log.error "Error calculating sun altitude time: ${e}"
        sendEvent(name: "sunAltitudeTime", value: "Error")
    }
}

// Helper method to get sunrise/sunset times
private getSunriseAndSunset(Map params) {
    def date = params.date ?: new Date()
    def location = params.location ?: new Date().location
    
    def calendar = Calendar.getInstance(location.timeZone)
    calendar.time = date
    
    def sunriseSunset = location.sunriseSunset(calendar)
    return [
        sunrise: sunriseSunset.sunrise,
        sunset: sunriseSunset.sunset
    ]
}