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

OK thats consistent to what you are saying but I would have thought at 12.14am here the moon would be up?

Sun azimuth: 188.079278958
Sun elevation: -72.973945699
Moon azimuth: 162.060936626
Moon elevation: -75.9157767755

as you can see on pictures i made ytd, moon was visible in australia and chile but not in dakar…
and moon and sun have been on very close trajectory.

everyday is moon on different trajectory, look ouside if there is moon and your picture is missing it - something wrong with your config :slight_smile:

Your elevation is negative. The moon is not visible.
The moon is not always out at night. It depends on it’s orbit position and the position of the earth around the sun. I am sure you have seen the moon in the daytime before.

OK thanks guys the sun is up and I have it on the map and a shadow so your probs right it was cloudy last night and couldnt verify the moon location but that may have been cos it wasnt there!

What I would love to use this for is to detrmine where the sun is and if within a certain range on the map (this could be static) then lower my blinds, and as the sun moves around raise them back up again - any ideas? Im using time for this but as we are in winter here the times are very different to what they would be in summer

Hi Andrew,
You could use the Azimuth of sun for this, have a look on following:

Thank for you help but try as I might I still cannot get it to work as expected. The only reason I changed my timezone in my original post was to prove something is off with the southern hemisphere. Time zone on my server is Australia/Queensland, Time zone in script is Australia/Queensland but something is still not right and I do not know what.

My assumption is it’s the script as I can get the expected results if I ‘pretend’ I’m in the northern hemisphere.

Brisbane Lat / Long with Brisbane TZ (no hour segments) - it’s night time here in Brisbane so expect no sun

Brisbane Lat / Long with New York TZ (no hour segments)

New York Lat / Long with Brisbane TZ (hour segments working)

New York Lat / Long with New York TZ (hour segments working)

The only pattern I see is everything with a brisbane (or southern hemisphere) lat / long is not working as expected. If I lived in New York everything would be good.

see my post above, Sydney is on Southern hemisphere and working just fine when everything is correctly configured.

If you are missing dash hours indicator, it’s 99% because your time is wrong (not TZ, time on you OH server!)

this is Brisbane right now… obviously there is no sun its 22:39 there

config for me would be:

LATTITUDE = -27.518157
LONGITUDE = 153.021851
TIMEZONE = 'Australia/Queensland'

and because I do have different time on my server, I need to do this:

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

if you have messed up OH server time, you will be missing hour circle, so … go to your ssh, and check that your local server time matches

Another Aussie here
Ive run date command and it shows correct time and timezone as well as paperui and both are correct.
Ive never had hour circle showing hours…

well, for me australia is working just fine, so am not saying script cant have issue, but as far as I can test, it does not.

actual servertime does not mean much if shown in paperui as it is affected by paperui setting.

what really and only matters is what time returns python datetime.now on machine from which you are running the script.

I’m going to try again this evening based on kriznik’s feedback, I’m sure all my TZ’s are correct but will check. I’ll also look at changing everything to north, including server TZ to compare.

Ive output self.now and it it correct as well. Im not sure wht i dont get the hours but im not really fussed its great to visualise the shadow which is really what I am after

PM your lat/long, tz and time on server
I’ll try it on my environment

Thanks for the widget. Its a nice widget.

The first time I made mistake and now I can get it right. I deleted the svg file but it didn’t work either. It stays the old way like this:

image

try to clear your browser cache… or ctrl+f5 or whatever is for your browser to force it for renew cached picture

So I’ve been looking into that southern hemisphere issue and it does look like it really has some difficulties here and there.
So here is updated version which (should) work, please let me know if it does not correctly for some lat/lon combinations

thanks

actual version of Aug 2019

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 = 36.174301
LONGITUDE = 19.099455
ALTITUDE = 0.0
TIMEZONE = 'Europe/Athens'
TOWN = 'Sea'

# Shape of the house in a 100 by 100 units square
SHAPE = [{'x': 16.37, 'y': 62.07}, \
		{'x': 29.57, 'y': 39.87}, \
		{'x': 38.68, 'y': 74.68}]
	
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())
        #for debug purposes
		#self.now = timezone.localize(datetime.now() + timedelta(hours=7) + timedelta(minutes=30))
		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)])

			if self.debug:
				print(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,orig_start,orig_end,attrs=None):
		
		if(LATITUDE<0):
			start = orig_end
			end = orig_start
		else:
			start = orig_start
			end = orig_end

		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("")
			print("real sun position: " + str(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

		if self.debug:
			print("")
			print("House:")

		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), "angle: ", str(round(angle,7)).ljust(10), "dist: ", str(round(distance)).ljust(10))
			i = i + 1

		if self.debug: 
			print("")
			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"')

			if self.debug:
				print(DEGS[i])


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

Will try it out and report back - thanks!

OK ive copied in the new script but no hour band still - although it is noght time should I get it during the night?

with data you sent me, it should look like this now:

I had been using the original script for a while, so when I came upon the updated code eliminating persistence, I copied the code from post #215 (actual version of Aug 2019) and pasted it into a new file in VS Code (no errors show up), but I am getting this:

[20:54:32] openhabian@openHABianPi:~$ python /etc/openhab2/scripts/shaddow.py update
  File "/etc/openhab2/scripts/shaddow.py", line 106
    def decdeg2dms(self,dd):
                           ^
IndentationError: unindent does not match any outer indentation level

The only thing I added to the script is the LAT, LON & ALT, along with my house coordinates, my time zone (America/New_York) and the item names I use for Sunset and Sunrise Azimuth. I did search through each line looking for trailing spaces and deleted a few. I also deleted some tabs on blank lines, but nothing seems to be working. I cannot see anything wrong with line 106, and nothing is coming up in VS Code…

Any help would be appreciated.

~John

EDIT: Didn’t have Python Extensions installed on VS Code… the above has been fixed, but now I am getting this:

[21:26:17] openhabian@openHABianPi:/etc/openhab2/scripts$ python /etc/openhab2/scripts/shaddow.py update
Traceback (most recent call last):
  File "/etc/openhab2/scripts/shaddow.py", line 5, in <module>
    import pytz
ImportError: No module named pytz
[21:26:26] openhabian@openHABianPi:/etc/openhab2/scripts$ sudo pip3 install pytz --upgrade
Requirement already up-to-date: pytz in /usr/local/lib/python3.4/dist-packages
Cleaning up...

VS Code is showing unresolved import for pylunar & astral and undefined variable for ‘Astral’ and ‘Location’
This is the top of my script:

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

I’m out of ideas…

pip install pytz

you installed pytz for root to python3, but you are executing python2 as non root