Incident Angle of Sunlight

EDIT:

I have made a new post with a much better control method: Automatic blinds / sunscreen control based on sun platform

Please let me know what you think!


Hi,

I made a template sensor to calculate the incident angle of sunlight based on position of the sun and position of any window in your house. I found that this is quite useful to control things like automatic blinds or sunscreens, which solely depend on the amount of sunlight coming in.

I figured that the amount of direct sunlight hitting the window depends on how ‘perpendicular’ the sun is with respect to the window. If the sun were to be directly in front of the window at 90 degrees with respect to the window then most sunlight would enter your room. A way to measure this is to consider the window as a plane and it’s normal vector the ‘best’ angle of incidence. The closer the sun’s angle of incidence is to your window the more light will enter the room. So I used some basic vector algebra to calculate this ‘angular similarity’.

It uses the sun.sun attributes azimuth and elevation to calculate the vector that points to the sun. Then you also need to fill in the elevation/azimuth from your window. Elevation is measured with respect to the center of the window so for a regular kitchen window this is 0 degrees. Azimuth can be found by using sun-earth tools: https://www.sunearthtools.com/dp/tools/pos_earth.php and selecting the polyline mode and measure the azimuth angle of your window (Dir).

# sun-window angular similarity
- platform: template
  sensors:
    sun_window_ang_similarity:
      friendly_name: Sun-window angular similarity
      unit_of_measurement: 'degrees'
      value_template: >
        {% set deg2rad = pi/180 %}

        {% set sun_azi = state_attr('sun.sun', 'azimuth') | int %}
        {% set sun_ele = state_attr('sun.sun', 'elevation') | int %}

        {% set sun_x = cos(sun_azi*deg2rad)*cos(sun_ele*deg2rad) %}
        {% set sun_y = sin(sun_azi*deg2rad)*cos(sun_ele*deg2rad) %}
        {% set sun_z = sin(sun_ele*deg2rad) %}

        {% set win_azi = 153 %}
        {% set win_ele = 0 %}

        {% set win_x = cos(win_azi*deg2rad)*cos(win_ele*deg2rad) %}
        {% set win_y = sin(win_azi*deg2rad)*cos(win_ele*deg2rad) %}
        {% set win_z = sin(win_ele*deg2rad) %}

        {% set dot = sun_x*win_x + sun_y*win_y + sun_z*win_z %}
        {% set norm_win = sqrt(win_x**2 + win_y**2 + win_z**2) %}
        {% set norm_sun = sqrt(sun_x**2 + sun_y**2 + sun_z**2) %}
        {% set cos_sim = dot/(norm_win*norm_sun) %}

        {% set ang_sim = 1 - acos(cos_sim)/pi %}
        {{ ang_sim | round(3) }}

This calculates a value between 0 and 1. A value of 1 means that the sun is perfectly perpendicular to your window and you will most likely be blinded. A value of 0 means that no light will enter the window.

17 Likes

This is great, thanks!

1 Like

This is indeed a great idea! I tried at first to adapt your idea with the normal vector of Window and sun, but it seemed too precise, because they would almost never be identical, unless in winter with a window pointing east or north and then only for probably one day.

So I created a node red flow which simply checks if the sun is inside the azimuth angles 90° before and after the orientation of my window and than above a given elevation of some obstacles and creates an binary sensor for it. Than I realized, that a house in front of my window is blocking the sun after a given azimuth anyway, so I took that one with the appropriate elevation.

Here is the flow for one of my windows.

[{"id":"d115352655d4a318","type":"ha-entity","z":"43831fb44d4043ee","name":"binary_sensor.schlafzimmer_sonne_trifft_fenster","server":"be104a93.6e2ed8","version":1,"debugenabled":false,"outputs":1,"entityType":"binary_sensor","config":[{"property":"name","value":"Schlafzimmer Sonne trifft Fenster"},{"property":"device_class","value":""},{"property":"icon","value":""},{"property":"unit_of_measurement","value":""}],"state":"payload","stateType":"msg","attributes":[],"resend":true,"outputLocation":"payload","outputLocationType":"none","inputOverride":"allow","x":720,"y":4240,"wires":[[]]},{"id":"59ece849c123b2ac","type":"function","z":"43831fb44d4043ee","name":"Azimut- und Höhenwinkel prĂŒfen","func":"// Azimut- und Höhenwinkel (elevation) der Objelte, welche die Sonne verdecken. \n// Höhenwinkel mĂŒssen noch gemessen werden, nur SchĂ€tzungen.\n// ab azi 220°, ele 0° bis azi 267°, ele 5° oder azi 151°, ele 0°.\n\n// Variable fĂŒr die Sonne aus msg.\nlet sonne = {azi: msg.data.attributes.azimuth, ele: msg.data.attributes.elevation};\n\n// Erst Azimutwinkel prĂŒfen, wenn im Rahmen Höhe prĂŒfen.\nif (sonne.azi >= 220 && sonne.azi <= 267) {\n    if (sonne.ele >= 5) {\n        msg.payload = \"on\";\n    } else {\n        msg.payload = \"off\";\n    }\n} else {\n    msg.payload = \"off\";\n}\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":340,"y":4240,"wires":[["d115352655d4a318"]]},{"id":"4bf622609233eba8","type":"poll-state","z":"43831fb44d4043ee","name":"Sonne","server":"be104a93.6e2ed8","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"updateinterval":"60","updateIntervalUnits":"seconds","outputinitially":false,"outputonchanged":true,"entity_id":"sun.sun","state_type":"str","halt_if":"","halt_if_type":"str","halt_if_compare":"is","outputs":1,"x":90,"y":4240,"wires":[["59ece849c123b2ac"]]},{"id":"be104a93.6e2ed8","type":"server","name":"Home Assistant","legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":false,"cacheJson":true}]

So, @langestefan thank you for your idea! I was trying to fix this problem for some time and my workaround so far was to use a time between node, but that was rather unsatisfactory.

1 Like

Good to read it’s useful for you. Indeed, the template provides a normalized value and does not take into account buildings blocking the field of view.

The idea is that in the winter the sun’s elevation is lower, thus the value returned by the template will be closer to 1 and you should lower the sunscreen more. You need to figure out empirically what threshold you want to set to trigger your sunscreen. You can follow the output of this sensor regardless of any buildings blocking the window, as long as the sunscreen is at a low enough position when the sun is up.

I see you hardcoded the elevation threshold for your binary sensor, this could work fine but do keep in mind that the time of day when this elevation is reached can change considerably in summer vs winter:

This becomes a problem when the azimuth is too low or too high for the sun to hit your window ‘enough’ but the elevation has exceeded the threshold :slight_smile:

Thanks for your answer. I would far more like to use vector algebra, because it is far more elegant in my opinion, but it seemed easier for me to find azimuth and elevation, for which the sun will hit my window, rather then try different values between 0 and 1. But I only wanted a binary sensor.

My situation is a bit different. I have different conditions, which all need to be fulfilled for the blinds to shut (heat in the room, weather forecast, radiator setpoint and the sun hitting the window). And it is either blinds open or closed, because I use them to keep the heat out and only have vertical blinds.

Yes the elevation will change over the year, but I first check the azimuth and if that is in the range in which the sun can hit that window, it will check the elevation, and if that is above that of the horizon or above the buildings, the binary senor will be on.

Thanks for sharing this, @langestefan. I’m playing a lot with this in the past week trying to find a good automation for my covers.

So I’m curious then
 Are you using only this to set your covers?

I’m trying to add info about the weather (cloudy vs sunny vs fog
 and visibility, which is normally available from any weather integration) and also to put some limits (like the value will be always 0 if the sun is below the horizon), etc


Any other ideas?

I actually made this for someone else, thought I would share it here too. My covers are not automated.

To get it completely customized to your liking is quite tricky. As stated above it also depends on any buildings blocking the window, seasonality etc. Personally I would use a binary input to decide whether or not to lower the cover, then use my template to decide how far to lower them.

For the binary sensor you may use any number of sensor inputs, but I would try to keep it simple. If you use irradiance as input for your binary sensor your can set a threshold to decide whether or not there is enough light. But these measurements tend to not be very reliable due to grid based calculations and dynamic weather. Ofcourse you can also directly measure amount of ambient light via a lux sensor, but these will be affected by lowering the covers. Perhaps some kind of weather station on your roof to measure light would be best
 or a light sensor in some window in the attic

1 Like

Ok this thread makes me feel stupid lol. Can somebody break this down for me. My wife would like me to close the blinds so her furniture does not get faded and this seems to be part of the answer.
So I goto the map calculator and I draw a line out of the “line of sight” out that window?
This then the Azimuth angle which my google foo states is an engineering term for direction facing?
Mine faces southwest.
In terms of elevation maybe b/c most house windows may be close to the ground that is zero but maybe if you were in an high story apartment building on mountain this would be more important?
I think I understand the part about the value reaching one when the sun is dead on facing the window. So perhaps an automation to close the blinds between a range would be the way to automate off this.
This thread makes me really understand my lack of understanding of celestial events but I am keenly aware of the fact that where I live the length of day and position of the sun in the sky changes alot depending on the time of year

Yep, azimuth is just the direction the window is facing. So north would be 0 (or 360) degrees, east 90 degrees, south 180 degrees and west 270 degrees. If your window faces exactly southwest then the azimuth would be 225 degrees (halfway between south and west is 45 degrees, so 225 total).

By elevation I just mean the vertical angle of the window, like on this picture (the person is the window):

Because the sun is so far away the actual height of the window is neglible even in an apartment building, the only thing that matters is the orientation of the window itself. The formula is such that if you have a completely vertical window the elevation should be 0 degrees. So if the sun is exactly on the horizon, the elevation of the sun will also be 0 degrees and so it will shine directly into your window.

I think the best way to figure out how to use it is to just play around with it a little bit. The sensor does not take into account hills or other object obstructing the sun light. You can use it as a threshold but you could also have some finer control by setting how far the sunscreen has to close based on the sensor’s value.

Hello! Thanks for your code, but I think something is not right, at least in my case. I just transformed the final value to a percentage, so instead of getting 0-1 I get 0-100%:
Replaced {{ ang_sim | round(3) }} for {{ (ang_sim * 100) | round(0) }}

and I get something like this, for these windows:
“Frente”:

        {% set win_azi = 225 %}
        {% set win_ele = 0 %}

“Traseira”:

        {% set win_azi = 45 %}
        {% set win_ele = 0 %}

image

I am using always a value of elevation of 0, maybe I didn’t get the point of that and maybe that is the problem, because I get maximum value in “Traseira” at 5.00am, before sunrise. This does not seem to take in account that the sun is down or not, what am I doing wrong? Can you help, please? And thanks again for your code, I would really like to use it the right way.

Hello. I think your graphs make sense, the azimuth of your windows differ by exactly 180 degrees. Indeed this sensor does not check whether the sun is up or down, if you want that you can easily add it. The sensor just gives a value, what you want to use it for is up to you.

1 Like

Thanks.
I replaced the last line of your code and I’m going to try to see the results.
{{ ((ang_sim * 100) | round(0)) if (sun_ele > 0) else 0 }}
it really doesn’t matter the value, each implementation must analyse the threshold that is needed for each window, and probably cross this with weather conditions ou sun intensity (I use my solar power production sensor to estimate outside luminosity) so I can do something like:
{{ ((ang_sim * 100 * states('sensor.calculated_outdoor_illuminance')|int(default=0)/10000) | round(0)) if (sun_ele > 0) else 0 }}
This sure helps! Thanks again!

Thanks a lot for sharing, Stefan!
I used to have FHEM running for my home automation and I had made myself a quite similar script in perl. Now I was trying to migrate it to Home Assistant but all this Jinja code and YAML indententation rules drove me crazy
 :woozy_face:

Your script made my day as it did the job right out of the box, nice work!
Let me just ask one question for clarification. Our house has these ‘Velux’-type skylights installed in a roof with an angle of 45 degrees - so in my case the definition would be {% set win_ele = 45 %}, correct? (At least that would be my guess if I got the vector math right
)

Hi, good to hear. Yes 45 degrees elevation would be correct :slight_smile:

Thanks for your reply. I have tested the sensor script for a few days now and I noticed that it returns a certain amount of angular similarity even when the sun is on the opposite side of the house. (see log graph below)
grafik

Even though this looks perfectly correct form a mathematical perspective (as far as I can tell), it was not quite what I needed for controlling the covers. So I tried some modifications and ended up using your cos_sim value combined with some extra conditions. As the dot product returns negative values if the vectors are pointing in opposite directions (sun illuminating the other side of the house), I had to ‘cut the curves’ for these undesired values and for negative sun elevations. To achieve this, I replaced the two last lines of your code by this:

{{ ((cos_sim * 100) | round(0)) if (sun_ele > 0 and cos_sim > 0 ) else 0 }}

This is what the resulting graph looks like:
grafik

I think the curves now represent the percentage of sun exposure of the several facades quite nicely. For example, you can see how the exposure of the front side (blue line) decreases steadily from sunrise until around 11:30 when the sun fully lights the left face (purple line). From that moment on, the front side is facing the sun no more and exposure of the aft face (cyan line) begins to rise.
The lines representing the roof areas (yellow/brownish and blueish/violet) seem to work as well. They also intersect at 11:30, but due to the 45 degree roof tilt, the sun begins to light the aft roof already at 10 o’clock and continues to light the forward roof until around 13:45. This agrees well with the observations I made when tracking the actual sun exposure.

I am looking forward to getting an outdoor brightness sensor (as proposed in this previous post), I will then further refine the logic by taking sensor values into account as well. Thanks to y’all for your ideas and inspiration!

1 Like

Hi, thanks for sharing. Indeed I did not take into account from which side of the window the sun is hitting. Glad to see you found a way that works. Are you already using it to control your covers? Mind sharing that too?

Ps. this post nicely explains why I chose to calculate the angular similarity rather than the cosine similarity (which you are using now, I guess it also works fine): vector spaces - Cosine similarity vs angular distance - Mathematics Stack Exchange

came here because of your recent post in Dutch Domotics Discord

this is some impressive templating you are doing here, or, even more, some impressive math.

Do I understand this correctly, in that I can use this on any window, by only adjusting the orientation of that window (mine are all perpendicular :wink: so don’t need any other consideration) ?

Seems quite a complex system compared to a simple and (always spot on) light sensor in all honestly, but I do like the mathematical approach, just like we have that for virtual daylight


That is a nice reference, I learned a lot of new stuff about angular functions now

It’s been quite a while since my uni math courses, but I think I mostly understood the point that is made. I mainly ended up with the cosine similarity because it felt a bit more intuitive for me. (I like how a value of 1 translates to full exposure and values of zero or below indicate no exposure.) I can not tell for sure if the discussed loss of precision at low angles and the more differentiated weighing is relevant in terms of solar radiation. Most probably you would also have to take into account how much of it is absorbed/reflected across different angles to make things more precise.

But now I’m getting close to nit-picking and such detailed considerations are (most likely) beyond the scope of most of us here I guess. I don’t want to turn this into a maths/physics thread, let’s keep these discussions in the respective forums
 At the end of the day, we’re just trying to automate these bloody covers, right? :wink:

I have way too many other things going on at the moment, so unfortunately I cannot spend as much time on home automation as I’d like to
 Nevertheless I spend more time on it than my wife would like me to
 :rofl:

So there is nothing to share so far, but I will keep an eye on this thread and come back if I’ve had time to make up some automation ideas. Might take a while though
 :man_shrugging:

Basically yes. As the calculations just rely on window orientation (in terms of azimuth and elevation/tilt) and sun position, there’s nothing more you have to care about in the first place. Just give it a try and then make your own observations if the calculations match the real situation.

Of course there’s potential for further refinement from that starting point, e.g. taking into account the shadowing effects of trees or other buildings around your house. But that would be a topic for a new thread then, I guess
 :grin: