Show Current Sun Position and Shadow of House (Generate SVG)

Hi Vincent,

Just updated the script with the “print (ts)”, and here’s the log output:

python /etc/openhab2/scripts/shaddow.py update
1522389308.06
120.558989318
21.6981129979
Done in 0.64107298851 seconds

Ok, now compare the 1522389308 with the actual time
This number is a unix timestamp, you can paste it in

https://www.unixtimestamp.com/

Do it again with a “fresh” value

I see :slight_smile: I learn new stuff everytime i visit this forum. It’s great!! :slight_smile:

So, I pasted a fresh value in the link provided.

My value= 1522407311.87
Converted = 03/30/2018 @ 10:55am (UTC)
Local time when executed= 12:55am

So It seems I’m 2 hours behind in the astral setup.

Thanks
Chris

Cool, 2 hours…
Now change the script as follow a little bit further down in the code:

                ...
		self.sunrise_azimuth = float(self.astr.solar_azimuth(self.sun['sunrise'], LATITUDE, LONGITUDE))
		self.sunset_azimuth = float(self.astr.solar_azimuth(self.sun['sunset'], LATITUDE, LONGITUDE))
		utc_offset = (datetime.fromtimestamp(ts) - datetime.utcfromtimestamp(ts)).total_seconds()/3600
		print(utc_offset)
		for h in xrange(0,24,HOURS):
			t = datetime.combine(date.today(), datetime.min.time()) + timedelta(hours=-utc_offset+h-24)
                ...

add the print(utc_offset) to check it. It should be 2 in your case but I suspect, it’s only 1

Here are my results after adding the “print(utc_offset)”

python /etc/openhab2/scripts/shaddow4.py update
1522421206.59
263.313925336
9.08693431761
2.0
Done in 0.615770101547 seconds

Very odd
I am starting to suspect there is a bug in astral
Let me sleep on it

okay. Thanks for your time and help.
Looking forward to hear from you, and possibly a clever solution :slight_smile:

Hi Vincent. Not to push or anything, but I’m just curious if you’ve though some more on my little astral issue? :slight_smile:

Sorry, easter week-end and all.
I was wondering, perhaps wrongly, but just to make sure…
From your post #84, there is 30d difference betwwen what astral gives you and what OH astro binding gives.
30 degrees is 2 hours of sun movement, give or take.
So either astral is wrong or OH is wrong…
So I have checked in suncalc.org for the 26/03/2018 at 19:38 and it appears that astral is wrong… Mmmmmh… By 2 hours… Mmmmmmmmmmhhhhh…

So let’s try to cheat astral
change that…

		self.debug = False 
		self.oh = openhab()
		self.astr = Astral()
		self.l = Location(('HOME', 'WATFORD', LATITUDE, LONGITUDE, 'Europe/London', ALTITUDE))
		self.sun = self.l.sun()
		ts = time.time() - (2 * 86400)
		self.azimuth = float(self.astr.solar_azimuth(datetime.fromtimestamp(ts), LATITUDE, LONGITUDE))
		print(self.azimuth)
		self.elevation = float(self.astr.solar_elevation(datetime.fromtimestamp(ts), LATITUDE, LONGITUDE))
		print(self.elevation)

We basically take 2 hours off the time we inject into astral
What happens?

Yeah, easter holiday makes it much harder to get time, for my home automation hobbies :smiley: But I have finally got around and tested your solution. Unfortunately there is no change though. I also tried to force reinstall the astral libraries. I can also confirm that OH2 has the right astro data, and astral is off by 2 hours.

It seems that whatever i put in the location parameters, there are no changes in the output data:

self.l = Location(('HOME', 'XXXX', LATITUDE, LONGITUDE, 'Europe/XXXXX', ALTITUDE))

So with your original location data, WATFORD and Europe/London, That should be 1 hour difference from Oslo. But no change if I use Oslo or London :confused:

Try that:

self.debug = False 
		self.oh = openhab()
		self.astr = Astral()
		self.l = Location(('HOME', 'WATFORD', LATITUDE, LONGITUDE, 'Europe/London', ALTITUDE))
		self.sun = self.l.sun()
		ts = time.time()
                print(ts)
		ts = ts - (2 * 86400)
                print(ts)
		self.azimuth = float(self.astr.solar_azimuth(datetime.fromtimestamp(ts), LATITUDE, LONGITUDE))
		print(self.azimuth)
		self.elevation = float(self.astr.solar_elevation(datetime.fromtimestamp(ts), LATITUDE, LONGITUDE))
		print(self.elevation)

What does your log show?

Hi @vzorglub and @sjef86,

I’m also using Astral now and I also encountered the issue with the wrong timezone.
I found in the Astral documentation that you need to localize your timestamps by using pytz.
(see https://astral.readthedocs.io/en/latest/#note-on-localized-timezones)

This is my init function:

    def __init__(self):
        self.debug = False 
        self.astr = Astral()
        timezone = pytz.timezone('Europe/Brussels')
        self.l = Location(('HOME', 'TOWN', LATITUDE, LONGITUDE, 'Europe/Brussels', ALTITUDE)) # The HOME and TOWN values are not important. Replace Europe/London with your local time string
        self.sun = self.l.sun()
        now = timezone.localize(datetime.now())
        
        self.azimuth = float(self.astr.solar_azimuth(now, LATITUDE, LONGITUDE))
        self.elevation = float(self.astr.solar_elevation(now, LATITUDE, LONGITUDE))
        self.sunrise_azimuth = float(self.astr.solar_azimuth(self.sun['sunrise'], LATITUDE, LONGITUDE))
        self.sunset_azimuth = float(self.astr.solar_azimuth(self.sun['sunset'], LATITUDE, LONGITUDE))		
        for i in range(0,24):
            a = float(self.astr.solar_azimuth(timezone.localize(datetime.combine(date.today(), time(i))), LATITUDE, LONGITUDE))
            if (a == None): a = 0
            DEGS.append(float(a))

It is possible that you will need to import pytz and some parts of datetime (if not imported yet):

from datetime import datetime, date, time
import pytz

Maybe you also need to install pytz via pip.
By using the localize() the timestamp will be set to the correct timezone.
I compared the generated output and it matches the “old” method that uses OpenHAB data.

I hope this helps!

Greetings,
Frederic

1 Like

Thanks @FredericMa
That would not have occured to me as I am in GMT…

@sjef86
Does that work?

Hi @vzorglub and @FredericMa,

I tried to replace my existing code with yours, Frederic. But now I get this error:

 python /etc/openhab2/scripts/shaddow.py
Traceback (most recent call last):
  File "/etc/openhab2/scripts/shaddow.py", line 37, in <module>
    class shaddow(object):
  File "/etc/openhab2/scripts/shaddow.py", line 46, in shaddow
    self.l = Location(('HOME', 'TOWN', LATITUDE, LONGITUDE, 'Europe/Oslo', ALTITUDE))
NameError: name 'self' is not defined

@sjef86, @FredericMa

Works for me! Thanks

But I had to make a few changes:

Import section:

from __future__ import print_function

import math
# import time
from datetime import datetime, timedelta, date, time
import sys
import pytz
from astral import Astral
from astral import Location

Init function

	def __init__(self):

		self.debug = False 
		self.oh = openhab()
		self.astr = Astral()
		timezone = pytz.timezone('Europe/London')
		self.l = Location(('HOME', 'WATFORD', LATITUDE, LONGITUDE, 'Europe/London', ALTITUDE))
		self.sun = self.l.sun()
		now = timezone.localize(datetime.now())
		self.azimuth = float(self.astr.solar_azimuth(now, LATITUDE, LONGITUDE))
		self.elevation = float(self.astr.solar_elevation(now, LATITUDE, LONGITUDE))
		self.sunrise_azimuth = float(self.astr.solar_azimuth(self.sun['sunrise'], LATITUDE, LONGITUDE))
		self.sunset_azimuth = float(self.astr.solar_azimuth(self.sun['sunset'], LATITUDE, LONGITUDE))
		for i in xrange(0,24,HOURS):
			a = float(self.astr.solar_azimuth(timezone.localize(datetime.combine(date.today(), time(i))), LATITUDE, LONGITUDE))

Main function (At the bottom)

	t1 = datetime.now()
...
	t2 = datetime.now()

Note that I have removed the print statements
Change 'Europe/London' with 'London/Olso' as required
Did you check that pytz was installed?
sudo pip install pytz
Did you add the statement import pytz at the top of the script?

1 Like

Woeps, forgot those 2 indeed… I already got rid of the time import since I removed the duration calculation a while ago.
@sjef86 have you copied the exact code? Maybe something went wrong while copying and pasting?

@FredericMa
Hi there, thanks for you good find indeed!!
I copied the code manually, I don’t like copying and pasting because you don’t get inside the code and get to learn and understand it.
I got it to work after a few tries.

@sjef86
With python be very careful with trailing spaces at the end of lines and with your indents

Thanks again…

Thanks guys! I finally got it working here :slight_smile: having some indent problems after copy/paste. I added:

	t1 = datetime.now()
...
	t2 = datetime.now()

And now everything is working great!! :smiley:
Thanks once more

Br
Chris

diffToFasade was bedeuted das genau? Was muss ich eintragen ? Eine haus seite zeigt genau nach norden bei mir.

1 Like

The whole code without persistence and timezone corrected
You will need the Astral library (pip install Astral) and the pytz library (sudo pip install pytz)
svg generated is in the shared html folder

from __future__ import print_function

import math
# import time
from datetime import datetime, timedelta, date, time
import sys
import pytz
from astral import Astral
from astral import Location

from myopenhab import openhab
from myopenhab import mapValues
from myopenhab import getJSONValue


WIDTH = 100
HEIGHT = 100
PRIMARY_COLOR = '#1b3024'
LIGHT_COLOR = '#26bf75'
STROKE_WIDTH = '1'
FILENAME = '/etc/openhab2/html/shaddow.svg'
LATITUDE = XX.XXXXXX # Your Latitude
LONGITUDE = XX.XXXXX # Your Longitude
ALTITUDE = XXX # Your Altitude

# Shape of the house in a 100 by 100 units square

SHAPE = [{'x': 23.18, 'y': 19.35}, \
		{'x': 70.11, 'y': 15.53}, \
		{'x': 75.32, 'y': 52.49}, \
		{'x': 79.96, 'y': 52.38}, \
		{'x': 80.65, 'y': 65.59}, \
		{'x': 76.83, 'y': 66.05}, \
		{'x': 78.68, 'y': 77.64}, \
		{'x': 31.52, 'y': 82.16}, \
		{'x': 30.13, 'y': 73.24}, \
		{'x': 18.54, 'y': 74.63}, \
		{'x': 14.72, 'y': 46.70}, \
		{'x': 26.07, 'y': 44.61}]

HOURS = 1
DEGS = []

class shaddow(object):
	"""
	Shaddow Object
	"""
	def __init__(self):

		self.debug = False 
		self.oh = openhab()
		self.astr = Astral()
		timezone = pytz.timezone('Europe/London') # Enter your time zone
		self.l = Location(('HOME', 'TOWN', LATITUDE, LONGITUDE, 'Europe/London', ALTITUDE))
		self.sun = self.l.sun()
		now = timezone.localize(datetime.now())
		self.azimuth = float(self.astr.solar_azimuth(now, LATITUDE, LONGITUDE))
		print(self.azimuth)
		self.elevation = float(self.astr.solar_elevation(now, LATITUDE, LONGITUDE))
		print(self.elevation)
		self.sunrise_azimuth = float(self.astr.solar_azimuth(self.sun['sunrise'], LATITUDE, LONGITUDE))
		self.sunset_azimuth = float(self.astr.solar_azimuth(self.sun['sunset'], LATITUDE, LONGITUDE))
		for i in xrange(0, 24, HOURS):
			a = float(self.astr.solar_azimuth(timezone.localize(datetime.combine(date.today(), time(i))), LATITUDE, LONGITUDE))
			if (a == None): a = 0
			DEGS.extend([float(a)])

	def generatePath(self,stroke,fill,points,attrs=None):

		p = ''
		p = p + '<path stroke="' + stroke + '" stroke-width="' + STROKE_WIDTH + '" fill="' + fill + '" '
		if (attrs != None): p = p + ' ' + attrs + ' '
		p = p + ' d="'
		for point in points:
			if (points.index(point) == 0):
				p = p + 'M' + str(point['x']) + ' ' + str(point['y'])
			else:
				p = p + ' L' + str(point['x']) + ' ' + str(point['y'])
		p = p + '" />'

		return p

	def generateArc(self,dist,stroke,start,end,attrs=None):

		p = ''
		try:
			angle = end-start
			if (angle<0):
				angle = 360 + angle

			p = p + '<path d="M' + str(self.degreesToPoint(start,dist)['x']) + ' ' + str(self.degreesToPoint(start,dist)['y']) + ' '
			p = p + 'A' + str(dist) + ' ' + str(dist) + ' 0 '
			if (angle<180):
				p = p + '0 1 '
			else:
				p = p + '1 1 '
			p = p + str(self.degreesToPoint(end,dist)['x']) + ' ' + str(self.degreesToPoint(end,dist)['y']) + '"'
			p = p + ' stroke="' + stroke + '"'
			if (attrs != None): 
				p = p + ' ' + attrs + ' '
			else:
				p = p + ' stroke-width="' + STROKE_WIDTH + '" fill="none" '
			p = p + ' />'
		except:
			p = ''

		return p	

	def degreesToPoint(self,d,r):

		coordinates = {'x': 0, 'y': 0}
		cx = WIDTH / 2
		cy = HEIGHT / 2 
		d2 = 180 - d
		coordinates['x'] = cx + math.sin(math.radians(d2))*r
		coordinates['y'] = cy + math.cos(math.radians(d2))*r

		return coordinates

	def generateSVG(self):

		realSun = self.degreesToPoint(self.azimuth, 10000)
		if self.debug:
			print(realSun)
			
		sun = self.degreesToPoint(self.azimuth, WIDTH / 2)

		minPoint = -1
		maxPoint = -1

		i = 0

		minAngle = 999
		maxAngle = -999
		for point in SHAPE:
			#Angle of close light source
			angle = -math.degrees(math.atan2(point['y']-sun['y'],point['x']-sun['x']))
			#Angle of distant light source (e.g. sun)
			angle = -math.degrees(math.atan2(point['y']-realSun['y'],point['x']-realSun['x']))
			distance = math.sqrt(math.pow(sun['y']-point['y'],2) + math.pow(sun['x']-point['x'],2))
			if (angle<minAngle): 
				minAngle = angle
				minPoint = i
			if (angle>maxAngle): 
				maxAngle = angle
				maxPoint = i
			point['angle'] = angle
			point['distance'] = distance
			if self.debug:
				print(str(i).ljust(10),":", str(point['x']).ljust(10), str(point['y']).ljust(10), str(round(angle,7)).ljust(10), str(round(distance)).ljust(10))
			i = i + 1

		if self.debug: 
			print("Min Point = ",minPoint)
			print("Max Point = ",maxPoint)
			print("")

		i = minPoint
		k = 0
		side1Distance = 0
		side2Distance = 0
		side1Done = False
		side2Done = False
		side1 = []
		side2 = []
		while True:
			if (side1Done == False):
				side1Distance = side1Distance + SHAPE[i]['distance']
				if(i != minPoint and i != maxPoint): SHAPE[i]['side'] = 1
				if (i == maxPoint): side1Done = True
				side1.append( { 'x': SHAPE[i]['x'], 'y': SHAPE[i]['y'] } )
			if (side1Done == True):
				side2Distance = side2Distance + SHAPE[i]['distance']
				if(i != minPoint and i != maxPoint): SHAPE[i]['side'] = 2
				if (i == minPoint): side2Done = True
				side2.append( { 'x': SHAPE[i]['x'], 'y': SHAPE[i]['y'] } )

			i = i + 1
			if( i > len(SHAPE)-1): i = 0

			if (side1Done and side2Done): break

			k = k + 1
			if (k == 20): break

		svg = '<?xml version="1.0" encoding="utf-8"?>'
		svg = svg + '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">'
		svg = svg + '<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="-10 -10 120 120" xml:space="preserve">'

		minPointShaddowX = SHAPE[minPoint]['x'] + WIDTH * math.cos(math.radians(minAngle))
		minPointShaddowY = SHAPE[minPoint]['y'] - HEIGHT * math.sin(math.radians(minAngle))
		maxPointShaddowX = SHAPE[maxPoint]['x'] + WIDTH * math.cos(math.radians(maxAngle))
		maxPointShaddowY = SHAPE[maxPoint]['y'] - HEIGHT * math.sin(math.radians(maxAngle))

		shaddow = [ {'x': maxPointShaddowX, 'y': maxPointShaddowY } ] + \
				side2 + \
				[ {'x': minPointShaddowX, 'y': minPointShaddowY } ]
		
		svg = svg + '<defs><mask id="shaddowMask">'
		svg = svg + '	  <rect width="100%" height="100%" fill="black"/>'
		svg = svg + '	  <circle cx="' + str(WIDTH/2) + '" cy="' + str(HEIGHT/2) + '" r="' + str(WIDTH/2-1) + '" fill="white"/>'
		svg = svg + '</mask></defs>'

		svg = svg + self.generatePath('none',PRIMARY_COLOR,SHAPE)

		shaddow_svg = self.generatePath('none','black',shaddow,'mask="url(#shaddowMask)" fill-opacity="0.5"')

		if (self.elevation>0): 
			svg = svg + self.generatePath(LIGHT_COLOR,'none',side1)
		else:
			svg = svg + self.generatePath(PRIMARY_COLOR,'none',side1)

		if (self.elevation>0): svg = svg + shaddow_svg

		svg = svg + self.generateArc(WIDTH/2,PRIMARY_COLOR,self.sunset_azimuth,self.sunrise_azimuth)
		svg = svg + self.generateArc(WIDTH/2,LIGHT_COLOR,self.sunrise_azimuth,self.sunset_azimuth)

		svg = svg + self.generatePath(LIGHT_COLOR,'none',[self.degreesToPoint(self.sunrise_azimuth,WIDTH/2-2), self.degreesToPoint(self.sunrise_azimuth,WIDTH/2+2)])
		svg = svg + self.generatePath(LIGHT_COLOR,'none',[self.degreesToPoint(self.sunset_azimuth,WIDTH/2-2), self.degreesToPoint(self.sunset_azimuth,WIDTH/2+2)])

		for i in range(0,len(DEGS)):
			if (i == len(DEGS)-1):
				j = 0
			else:
				j = i + 1
			if (i % 2 == 0):
				svg = svg + self.generateArc(WIDTH/2+8,PRIMARY_COLOR,DEGS[i],DEGS[j],'stroke-width="3" fill="none" stroke-opacity="0.2"')	
			else:
				svg = svg + self.generateArc(WIDTH/2+8,PRIMARY_COLOR,DEGS[i],DEGS[j],'stroke-width="3" fill="none"')

		svg = svg + self.generatePath(LIGHT_COLOR,'none',[self.degreesToPoint(DEGS[0],WIDTH/2+5), self.degreesToPoint(DEGS[0],WIDTH/2+11)])
		svg = svg + self.generatePath(LIGHT_COLOR,'none',[self.degreesToPoint(DEGS[(len(DEGS))/2],WIDTH/2+5), self.degreesToPoint(DEGS[(len(DEGS))/2],WIDTH/2+11)])

		svg = svg + '<circle cx="' + str(sun['x']) + '" cy="' + str(sun['y']) + '" r="3" stroke="' + LIGHT_COLOR + '" stroke-width="' + STROKE_WIDTH + '" fill="' + LIGHT_COLOR + '" />'

		svg = svg + '</svg>'

		if self.debug:
			print(svg)

		f = open(FILENAME, 'w')
		f.write(svg)
		f.close()


def main():

	t1 = datetime.now()

	s = shaddow()

	args = sys.argv
		
	if(len(args) == 1):
		dummy = 0
		#print('\033[91mNo parameters specified\033[0;0m')
	else:
		if(args[1] == "update"):
			s.generateSVG()

	t2 = datetime.now()
	#print("Done in " + str(t2-t1) + " seconds")

if __name__ == '__main__':
	main()
1 Like