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

use latest code, which does not need any calculations from persistence

1 Like

I’m going to do the same thing as soon as possible!!
So your work is really really useful for me!

In final, my goal is to make OH2 alerts people in my home that the system is about to close or open the blinds …

Bye!

astro.things:

astro:sun:home [ geolocation=“XXX, XXX”, interval=300 ]
astro:moon:home [ geolocation=“XXX, XXX”, interval=300 ]

the astro binding is active

openhab> bundle:list
START LEVEL 100 , List Threshold: 50
ID │ State │ Lvl │ Version │ Name

196 │ Active │ 80 │ 0.10.0.oh240 │ Eclipse SmartHome Astro Binding

weird I didn’t get it
hope you guys see something I’m missing here
really appreciate your help, thanks so far

@kriznik
yes this works but it makes my whole work with influxdb and grafana needless:unamused:
With this script I’m 2 hours behind astral.
My local time is UTC + 2, any ideas how to change the shaddow.py

@kriznik
I use the script from this post and filled the constants with values from SunCalc pointing to my location. I changed the timezone to

timezone = pytz.timezone(‘Europe/Berlin’)

But the sun position in the svg is UTC and not TZ: Europe/Berlin DST CEST. It should be UTC + 2 (due to the dst). The timezone object is localized, this should be working :triumph:

for comparison
svg.
grafik

14:36:35.992 [INFO ] [clipse.smarthome.model.script.Shaddow] - 2019-07-21 14:36:35.793169
Sun azimuth: 209.804400066
Sun elevation: 54.1718247517
Moon azimuth: 354.069520686
Moon elevation: -45.2328082501
2019-07-21 14:36:35.923135
Done in 0:00:00.129966 seconds

and what exactly is wrong?
be aware that yellow dot is not representing sun position in relation to your house, but to the sky sunrise/sunset and hours.

especially at the evening it might look off, but it’s not. Shadow cast is calculated from distant light source so it’s very precise, sun is calculated from close light source because of the circle (and we know that earth is not doing perfect circle around the sun, right? :slight_smile: )

have you changed as well

self.l = Location(('HOME', 'YOUR TOWN', LATITUDE, LONGITUDE, 'Europe/Berlin', ALTITUDE))

?

1 Like

yes I have changed the parameter in the location function to my timezone (‘Europe/Berlin’).
Now checked the position of my house on SunCalc and it is not quite in the correct position as I coordinated it in shaddow.py. Ahh just 6-10 degree to the right and the rest is owed to the earth wobbling orbital ellipse.
I copied the position from google maps and it is not the same like on SunCalc.
Sorry for the trouble and thanks for physics extra lesson, I greatly appreciate it.
Keep going here, i love your great stuff

@kriznik can you please verify the self.now varaible in shaddow.py.

self.now = timezone.localize(datetime.now(),is_dst=True)
print(self.now)

due to the fact that datetime is server datetime (utc) the varibale self.now will always be in utc.
I guess the datetime.now() needs the timezone argument

dt_utc = datetime.datetime.now(datetime.timezone.berlin)

but I didn’t get it to work

why you have OH server in UTC?

self.now = timezone.localize(datetime.now())
self.nowUTC = datetime.utcnow()

I guess you have to adjust above code to something like

TimeZone timeZone = TimeZone.getTimeZone(“Europe/Berlin”)
self.setTimeZone(timeZone)

Are the files from the first post already updated with the new version without the need of any persistence (influxdb)?

this is the latest updated version I guess

@kriznik

why you have OH server in UTC?

:rofl: I dont’t know, after I had setted up the system I adjusted the regional settings in Paper UI.
Seems that this isn’t enough.
Now I added to EXTRA_JAVA_OPTS = -Duser.timezone=Europe/Berlin without success after reboot

TimeZone timeZone = TimeZone.getTimeZone(“Europe/Berlin”)
^
SyntaxError: invalid syntax

I also tried some datetime and timezone converting I found in the python libraires, without success.
I’m lost here, …again :sob:

go here and reconfigure your locale

or wait till the evening, I’ll be at my computer with proper dev tools, ipad suxx :slight_smile:

So, I’ve been playing with my setting and no, it’s working as expected, but I’m not running server in UTC tho, anyway it should not be an issue, you can add hour from UTC if you are in Berlin to your time like this:

self.now = timezone.localize(datetime.now() + timedelta(hours=1))

^^ by playing with hours, you can see where is sun during the day without need of waiting :slight_smile:
just jump into your server ssh and type: (adjust your path if needed)

python /etc/openhab2/scripts/shaddow.py update

refresh your browser at:

http://yourohserver-ip/static/shaddow.svg

And here is bit polished script, where you declare your timezone just once in header not twice as before:

from __future__ import print_function

import math

from datetime import datetime, timedelta, date, time
import sys
import pytz
import pylunar

from astral import Astral
from astral import Location

WIDTH = 100
HEIGHT = 100
PRIMARY_COLOR = '#1b3024'
LIGHT_COLOR = '#26bf75'
BG_COLOR = '#1a1919'
SUN_COLOR = '#ffff66'
SUN_RADIUS = 5

MOON_COLOR = '#999999'
MOON_RADIUS = 3
STROKE_WIDTH = '1'
FILENAME = '/etc/openhab2/html/shaddow.svg'

LATITUDE = 11.0000000
LONGITUDE = 12.000000
ALTITUDE = 5000.0
TIMEZONE = 'Pacific/Tongatapu'
TOWN = 'Something'

# Shape of the house in a 100 by 100 units square
SHAPE = [{'x': 16.37, 'y': 62.07}, \
		{'x': 28.32, 'y': 40.16}, \
		{'x': 29.57, 'y': 39.87}]
	
HOURS = 1
DEGS = []

## not really needed to edit anything below
class shadow(object):
	"""
	Shadow Object
	"""
	def __init__(self):

		self.debug = False 
		self.astr = Astral()
		self.l = Location(('HOME', TOWN, LATITUDE, LONGITUDE, TIMEZONE, ALTITUDE))
		self.sun = self.l.sun()

		timezone = pytz.timezone(TIMEZONE)
		self.now = timezone.localize(datetime.now())
		self.nowUTC = datetime.utcnow()
		
		self.sun_azimuth = float(self.astr.solar_azimuth(self.now, LATITUDE, LONGITUDE))
		print('Sun azimuth: ' + str(self.sun_azimuth))
		self.sun_elevation = float(self.astr.solar_elevation(self.now, LATITUDE, LONGITUDE))
		print('Sun elevation: ' + str(self.sun_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)])

		self.moon_info = pylunar.MoonInfo(self.decdeg2dms(LATITUDE), self.decdeg2dms(LONGITUDE))
		self.moon_info.update(self.nowUTC)
		self.moon_azimuth = self.moon_info.azimuth()
		print('Moon azimuth: ' + str(self.moon_azimuth))
		self.moon_elevation = self.moon_info.altitude()
		print('Moon elevation: ' + str(self.moon_elevation))

		if (self.sun_elevation>0): 
			self.elevation = self.sun_elevation
		else:
			self.elevation = self.moon_elevation


	#
	#
	#
	def decdeg2dms(self,dd):
		negative = dd < 0
		dd = abs(dd)
		minutes,seconds = divmod(dd*3600,60)
		degrees,minutes = divmod(minutes,60)
		if negative:
			if degrees > 0:
				degrees = -degrees
			elif minutes > 0:
				minutes = -minutes
			else:
				seconds = -seconds
		return (degrees,minutes,seconds)	

	#
	#
	#
	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,fill,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 (fill != None): 
				p = p + ' fill="' + fill + '" '
			else:
				p = p + ' fill="none" '
			if (attrs != None): 
				p = p + ' ' + attrs + ' '
			else:
				p = p + ' stroke-width="' + STROKE_WIDTH + '"'
			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_pos = self.degreesToPoint(self.sun_azimuth, 10000)
		realMoon_pos = self.degreesToPoint(self.moon_azimuth, 10000)
		if self.debug:
			print(realSun_pos)
			
		sun_pos = self.degreesToPoint(self.sun_azimuth, WIDTH / 2)
		moon_pos = self.degreesToPoint(self.moon_azimuth, WIDTH / 2)

		minPoint = -1
		maxPoint = -1

		i = 0

		minAngle = 999
		maxAngle = -999
		if(self.sun_elevation>0):
			angle_pos = sun_pos
			real_pos = realSun_pos
		else:
			angle_pos = moon_pos
			real_pos = realMoon_pos

		for point in SHAPE:
			#Angle of close light source
			angle = -math.degrees(math.atan2(point['y']-angle_pos['y'],point['x']-angle_pos['x']))
			#Angle of distant light source (e.g. sun_pos)
			angle = -math.degrees(math.atan2(point['y']-real_pos['y'],point['x']-real_pos['x']))
			distance = math.sqrt(math.pow(angle_pos['y']-point['y'],2) + math.pow(angle_pos['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">'

                # background
		svg = svg + '<circle cx="' + str(WIDTH/2) + '" cy="' + str(HEIGHT/2) + '" r="' + str(WIDTH/2-1) + '" fill="' + BG_COLOR + '"/>'

		minPointShadowX = SHAPE[minPoint]['x'] + WIDTH * math.cos(math.radians(minAngle))
		minPointShadowY = SHAPE[minPoint]['y'] - HEIGHT * math.sin(math.radians(minAngle))
		maxPointShadowX = SHAPE[maxPoint]['x'] + WIDTH * math.cos(math.radians(maxAngle))
		maxPointShadowY = SHAPE[maxPoint]['y'] - HEIGHT * math.sin(math.radians(maxAngle))

		shadow = [ {'x': maxPointShadowX, 'y': maxPointShadowY } ] + \
				side2 + \
				[ {'x': minPointShadowX, 'y': minPointShadowY } ]
		
		svg = svg + '<defs><mask id="shadowMask">'
		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)

		shadow_svg = self.generatePath('none','black',shadow,'mask="url(#shadowMask)" fill-opacity="0.5"')

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

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

		svg = svg + self.generateArc(WIDTH/2,PRIMARY_COLOR,'none',self.sunset_azimuth,self.sunrise_azimuth)
		svg = svg + self.generateArc(WIDTH/2,LIGHT_COLOR,'none',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,'none',DEGS[i],DEGS[j],'stroke-width="3" stroke-opacity="0.2"')	
			else:
				svg = svg + self.generateArc(WIDTH/2+8,PRIMARY_COLOR,'none',DEGS[i],DEGS[j],'stroke-width="3" ')

		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)])

		# moon drawing: compute left and right arcs
		phase = self.astr.moon_phase(self.now)
		if self.debug:
			print('phase: ' + str(phase))
		left_radius=MOON_RADIUS
		left_sweep=0
		right_radius=MOON_RADIUS
		right_sweep=0
		if (phase > 14):
			right_radius = MOON_RADIUS - (2.0*MOON_RADIUS* (1.0 - ((phase%14)*0.99 / 14.0)))
			if (right_radius < 0):
				right_radius = right_radius * -1.0
				right_sweep = 0
			else:
				right_sweep = 1
		
		if (phase < 14):
			left_radius = MOON_RADIUS - (2.0*MOON_RADIUS* (1.0 - ((phase%14)*0.99 / 14.0)))
			if (left_radius < 0):
				left_radius = left_radius * -1.0
				left_sweep = 1
				
		if (self.moon_elevation>0): 
			svg = svg + '<path stroke="none" stroke-width="0" fill="' + MOON_COLOR \
			+ '" d="M ' + str(moon_pos['x']) + ' ' + str(moon_pos['y']-MOON_RADIUS) \
			+ ' A ' + str(left_radius) + ' ' + str(MOON_RADIUS) + ' 0 0 ' + str(left_sweep) + ' ' + str(moon_pos['x']) + ' ' + str(moon_pos['y']+MOON_RADIUS) \
			+ '   ' + str(right_radius) + ' ' + str(MOON_RADIUS) + ' 0 0 ' + str(right_sweep) + ' ' + str(moon_pos['x']) + ' ' + str(moon_pos['y']-MOON_RADIUS) + ' z" />'

		# sun drawing
		if (self.sun_elevation>0): 
			svg = svg + '<circle cx="' + str(sun_pos['x']) + '" cy="' + str(sun_pos['y']) + '" r="' + str(SUN_RADIUS) + '" stroke="none" stroke-width="0" fill="' + SUN_COLOR + '55" />'
			svg = svg + '<circle cx="' + str(sun_pos['x']) + '" cy="' + str(sun_pos['y']) + '" r="' + str(SUN_RADIUS -1) + '" stroke="none" stroke-width="0" fill="' + SUN_COLOR + '99" />'
			svg = svg + '<circle cx="' + str(sun_pos['x']) + '" cy="' + str(sun_pos['y']) + '" r="' + str(SUN_RADIUS -2) + '" stroke="' + SUN_COLOR + '" stroke-width="0" fill="' + SUN_COLOR + '" />'

		svg = svg + '</svg>'

		if self.debug:
			print(svg)

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


def main():

	t1 = datetime.now()

	s = shadow()

	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()

but still, I’ll recommend you to reconfigure your OH server to be in your timezone properly, as it’s causing issues here and there.

1 Like

yes the timedelta does the trick even though you are absolutly right when server timezone is not matching the script timezone work around. I’ll set the server timezone to Europe/Berlin when I’m back home.
many thanks for your help.
I’m still wondering that adding timezone to extra_java_opts hasn’t any effect on OH

mainly because .py file is python and not interacting with OH in any way. Its standalone script, which is being called from OH.

correct time is everything, otherwise you’ll have to care about daylight saving manually and edit timedelta eventually.

:slight_smile:

1 Like

@kriznik - your script looks great and working for me (kind of) - I am in the southern hempsphere - what can I change to fix the sun position?

all you really need to do is to change these

LATITUDE = 11.0000000
LONGITUDE = 12.000000
ALTITUDE = 5000.0
TIMEZONE = 'Pacific/Tongatapu'
SHAPE = xxx

SHAPE is most difficult, really :wink:

+have correct time on your machine.
Rest will magically happen

it’s not originally my script, I’ve just made few adjustments/fixes, credits belongs to others mainly
But indeed thanks :wink:

I found this tool for plotting points on a image to get the SHAPE

https://www.mobilefish.com/services/record_mouse_coordinates/record_mouse_coordinates.php

I used google maps to get a north aligned floorplan, also i had to manually scale down the output plots and easy left right up down increments to centre for the final gui

1 Like

damn this is good, I wish I knew before I did my house point by point manually from Inkscape :smiley:
:+1: