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

@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:

hi, did you find a solution for triggering if/not a position is shaded or not ?

I was going to try some experiments with the SunSet Azimuth

No I haven’t I’m also still struggling to get it to show correctly in the first pkace

Ok so for Southern Hemisphere query - which way should my house be facing? North to top?

grab a google maps, face it as is there, setup lat/long, timezone and correct time on your server and you should be good to go

Hi!
I’d like to add a few things:

  1. shaddow.svg has origin (x= 0, y= 0) in the upper left corner.
    Instead Inkscape has it in the lower left corner.
    Consider to vertically flip the image before taking nodes coordinates.
  2. Nodes has to be written in counterclockwise order.
  3. Shaddow.py has a nodes number capacity limited to 20.
    If you have (like me) more than 20 nodes you have to modify line number 190 (approx):

if (k == 20): break
/----/
Now it’s working fine !!
Thanks @pmpkk and everybody contributing this thread!

Alex
_shaddow

2 Likes

I’m having the same issue as @Andrew_Pawelski with being in the southern hemisphere, and like Andew I cannot work out what to change. Any ideas?

@nathantfry I did as above - used google maps - put it into SHAPE, set lat, long and timezone and then adjusted the image x an y until it was in the middle of the circle and the sun matched what it looked like when i looked out the window

I dont have a moon on the circle (thought i saw one at some stage though