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

how hard it can be to follow simple guidelines…?

extremely hard. is my 4th attempt trying to make this work by following step by step the articles provided , plenty of missing points, plenty of confusion, various of solutions messes up the initial setup . no offence, i really appreciate people’s work but this is extremely fustrating . for example: the op provided 2 scripts , myopenhab.py seems to serve no purpose as the user did not added any mention about it and it’s execution … maybe i’m too stupid for this but it’s enough for me …

OH does not provide any script to generate svg of your house to my knowledge.
This guide expects you are already familiar with OH, folder structure etc. shell, python and stuff, it is not simple plug & play

but for people which do not want to read and learn, I’ve added some additional info to my python3 version post. It does include every info you need to get this working.

hello together, the idea of this is so great, that i finally created an account and i want to show my solution.

I have made an different approach, since i have no persistence yet and thougt, it must be possible to get the sun position by calculating it, with the astro binding and i don’t wanted to wait a whole day, to fill the database. I’m also not familiar with python, so i wanted a solution within a rule.

Here is the result, just a bunch of rules, some items and a standard picture in habpanel

(The rule for generating the svg is about 500 lines of code, so it takes a lot of time for openhab to update it)

Group gAstro "Astro Items"

DateTime ActualTime "aktuelle Uhrzeit"       (gAstro) {channel="ntp:ntp:local:dateTime"}
DateTime RiseStart "Startzeit Sonnenaufgang" (gAstro) {channel="astro:sun:home:rise#start"} 
DateTime SetEnd "Endzeit Sonnenuntergang"    (gAstro) {channel="astro:sun:home:set#end"}
Number:Angle PositionAzimuth "Azimut"        (gAstro) {channel="astro:sun:home:position#azimuth"}
Number:Angle SunElevation "Elevation"        (gAstro) {channel="astro:sun:home:position#elevation"}

Number:Angle SunrisePositionAzimuth "Sonnenaufgang Azimuth"  (gAstro)
Number:Angle SunsetPositionAzimuth "Sonnenuntergang Azimuth" (gAstro)

Number:Angle Azimuth0  "Azimuth 00:00 Uhr" (gAstro)
Number:Angle Azimuth1  "Azimuth 01:00 Uhr" (gAstro)
Number:Angle Azimuth2  "Azimuth 02:00 Uhr" (gAstro)
Number:Angle Azimuth3  "Azimuth 03:00 Uhr" (gAstro)
Number:Angle Azimuth4  "Azimuth 04:00 Uhr" (gAstro)
Number:Angle Azimuth5  "Azimuth 05:00 Uhr" (gAstro)
Number:Angle Azimuth6  "Azimuth 06:00 Uhr" (gAstro)
Number:Angle Azimuth7  "Azimuth 07:00 Uhr" (gAstro)
Number:Angle Azimuth8  "Azimuth 08:00 Uhr" (gAstro)
Number:Angle Azimuth9  "Azimuth 09:00 Uhr" (gAstro)
Number:Angle Azimuth10 "Azimuth 10:00 Uhr" (gAstro)
Number:Angle Azimuth11 "Azimuth 11:00 Uhr" (gAstro)
Number:Angle Azimuth12 "Azimuth 12:00 Uhr" (gAstro)
Number:Angle Azimuth13 "Azimuth 13:00 Uhr" (gAstro)
Number:Angle Azimuth14 "Azimuth 14:00 Uhr" (gAstro)
Number:Angle Azimuth15 "Azimuth 15:00 Uhr" (gAstro)
Number:Angle Azimuth16 "Azimuth 16:00 Uhr" (gAstro)
Number:Angle Azimuth17 "Azimuth 17:00 Uhr" (gAstro)
Number:Angle Azimuth18 "Azimuth 18:00 Uhr" (gAstro)
Number:Angle Azimuth19 "Azimuth 19:00 Uhr" (gAstro)
Number:Angle Azimuth20 "Azimuth 20:00 Uhr" (gAstro)
Number:Angle Azimuth21 "Azimuth 21:00 Uhr" (gAstro)
Number:Angle Azimuth22 "Azimuth 22:00 Uhr" (gAstro)
Number:Angle Azimuth23 "Azimuth 23:00 Uhr" (gAstro)

Switch UpdateAzimuth "Update Azimuth" (gAstro)
Switch UpdateSVG "Update SVG" (gAstro)

import java.time.ZonedDateTime

rule "Get Sunrise Position Azimut"
when
    Channel "astro:sun:home:rise#event" triggered START
then
    SunrisePositionAzimuth = PositionAzimuth    
end

rule "Get Sunset Position Azimut"
when
    Channel "astro:sun:home:set#event" triggered END

then
    SunsetPositionAzimuth = PositionAzimuth    
end

rule "System Start"
when
    System started
then
    UpdateAzimuth.sendCommand(ON)
end

rule "Update SVG when Azimuth Changed"
when
    Item PositionAzimuth received update
then
    UpdateSVG.sendCommand(ON)
end

rule "Update Azimuths once a day"
when
    Time is midnight
then
    UpdateAzimuth.sendCommand(ON)
end

rule "Update Azimuth"
when
    Item UpdateAzimuth received command ON
then
    val sunActions = getActions("astro","astro:sun:home")
    if(null === sunActions) {
        logInfo("actions", "sunActions not found, check thing ID")
        return
    } else {

        val sunSetEvent = "SUN_SET"
        val sunRiseEvent = "SUN_RISE"
        val today = ZonedDateTime.now
        val sunSetEventTime = sunActions.getEventTime(sunSetEvent,today,"START")
        val sunRiseEventTime = sunActions.getEventTime(sunRiseEvent,today,"START")
        val sunSetAzimuth = sunActions.getAzimuth(sunSetEventTime)
        val sunRiseAzimuth = sunActions.getAzimuth(sunRiseEventTime)
        SunsetPositionAzimuth.sendCommand(sunSetAzimuth)
        SunrisePositionAzimuth.sendCommand(sunRiseAzimuth)

        val zdt = ZonedDateTime.now

        Azimuth0.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 0, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth1.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 1, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth2.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 2, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth3.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 3, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth4.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 4, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth5.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 5, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth6.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 6, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth7.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 7, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth8.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 8, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth9.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 9, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth10.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 10, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth11.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 11, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth12.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 12, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth13.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 13, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth14.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 14, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth15.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 15, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth16.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 16, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth17.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 17, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth18.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 18, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth19.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 19, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth20.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 20, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth21.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 21, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth22.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 22, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)
        Azimuth23.sendCommand(sunActions.getAzimuth(ZonedDateTime.of(zdt.getYear, zdt.getMonthValue, zdt.getDayOfMonth(), 23, 0, 0, 0, zdt.getZone())).toBigDecimal as Number)

    }

    UpdateAzimuth.sendCommand(OFF)
end

// =====================================================================================================================================
// Rule generiert die Vektorgrafik für den Hausgrundriss mit Sonnestand und Schatten
// =====================================================================================================================================
import java.io.File
import java.io.FileWriter
import java.lang.Math
import java.awt.Point

rule "Update Vectorgrafik"
when
    Item UpdateSVG received command ON
then

    val WIDTH = 100
    val HEIGHT = 100
    val LIGHT_COLOR = "#4ebbed" //"#1b3024" // hellblau
    val PRIMARY_COLOR = "#34485b" //Hellgrau    //"#253443" Hintergrundfabre Buttons
    val STROKE_WIDTH = "1"
    val doctype = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"
    val header ="<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\" height=\"250\" width=\"250\">\n"
    val footer = "\n</svg>"

    val SHAPE = newArrayList()
    SHAPE.add(new Point (3920,  680)) //  1. oben links
    SHAPE.add(new Point (6410,  510)) //  2. oben rechts
    SHAPE.add(new Point (6540, 2320))
    SHAPE.add(new Point (6640, 2310))
    SHAPE.add(new Point (6720, 3310))
    SHAPE.add(new Point (6620, 3310))
    SHAPE.add(new Point (6700, 4510))
    SHAPE.add(new Point (5750, 4570))
    SHAPE.add(new Point (5980, 7620))
    SHAPE.add(new Point (6050, 7610)) // 10 Eingang Haus 2 oben rechts außen
    SHAPE.add(new Point (6080, 8050)) // 11 Eingang Haus 2 unten rechts außen
    SHAPE.add(new Point (6000, 8060))
    SHAPE.add(new Point (6070, 9330)) // 13 unten rechts
    SHAPE.add(new Point (3600, 9500)) // 14. unten links
    SHAPE.add(new Point (3270, 4660))
    SHAPE.add(new Point (4190, 4620))

    var sunX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(PositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2)
    var sunY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(PositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2)

    var svg = ""
    svg += "<defs><mask id=\"shaddowMask\">\n"
    svg += "    <rect width=\"100%\" height=\"100%\" fill=\"black\"/>\n"
    svg += "    <circle cx=\"" + WIDTH/2 + "\" cy=\"" + (HEIGHT/2) + "\" r=\"" + (WIDTH/2-1) + "\" fill=\"white\"/>\n"
    svg += "</mask></defs>\n"
    // =====================================================================================================================================
    // Schatten berechnen
    // =====================================================================================================================================   
    var minPoint = -1
    var maxPoint = -1
    var minAngle = 999.0
    var maxAngle = -999.0
    var winkel = 0.0
    var abstand = newArrayList()

    var tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(PositionAzimuth.state.toString.substring(0, 5)))) * 10000
    var tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(PositionAzimuth.state.toString.substring(0, 5)))) * 10000

    var Point pSonne = new Point(tmpX.intValue(), tmpY.intValue())

    for(var i = 0; i < SHAPE.size() ; i++)
    {
        // Angle of distant light source (e.g. sun)
        winkel = -Math.toDegrees(Math.atan2(((SHAPE.get(i).getY()/100) - pSonne.getY()), ((SHAPE.get(i).getX()/100) - pSonne.getX())))

        abstand.add(Math.sqrt(Math.pow(pSonne.getY() - (SHAPE.get(i).getY()/100), 2) + Math.pow(pSonne.getX() - (SHAPE.get(i).getX()/100), 2)))
        if(winkel < minAngle)
        {
            minAngle = winkel
            minPoint = i
        }
        if(winkel > maxAngle)
        {
            maxAngle = winkel
            maxPoint = i
        }   
    }
        
    var side1Distance = 0.0
    var side2Distance = 0.0
    var side1Done = false
    var side2Done = false
    var side1 = newArrayList()
    var side2 = newArrayList()

    var whileNotDone = true
    var k = minPoint
    var l = 0

    while(whileNotDone)
    {
        if(side1Done == false)
        {
            side1Distance += abstand.get(k)
            if(k == maxPoint)
            {
                side1Done = true
            }
            side1.add(SHAPE.get(k))
        }
        if(side1Done == true)
        {
            side2Distance += abstand.get(k)
            if(k == minPoint)
            {
                side2Done = true
            }
            side2.add(SHAPE.get(k))
        }
        k += 1
        if(k > (SHAPE.size() - 1))
        {
            k = 0
        }
        if((side1Done && side2Done) == true)
        {
            whileNotDone = false
        }

        l += 1
        if(l >= 40)
        {
            whileNotDone = false
            logInfo("Debug", "l = " + l)
        }
    }

    val minPointShaddowX = (((SHAPE.get(minPoint).getX()/100) + WIDTH * Math.cos(Math.toRadians(minAngle)))) * 100
    val minPointShaddowY = (((SHAPE.get(minPoint).getY()/100) - HEIGHT * Math.sin(Math.toRadians(minAngle)))) * 100
    val maxPointShaddowX = (((SHAPE.get(maxPoint).getX()/100) + WIDTH * Math.cos(Math.toRadians(maxAngle)))) * 100
    val maxPointShaddowY = (((SHAPE.get(maxPoint).getY()/100) - HEIGHT * Math.sin(Math.toRadians(maxAngle)))) * 100

    val shaddow = newArrayList()
    shaddow.add(new Point(maxPointShaddowX.intValue(), maxPointShaddowY.intValue()))
    for(var i = 0; i < side2.size(); i++)
    {
        shaddow.add(side2.get(i))
    }
    shaddow.add(new Point(minPointShaddowX.intValue(), minPointShaddowY.intValue()))
    // =====================================================================================================================================
    // Schatten
    // =====================================================================================================================================
    var svgPath = ""
    svgPath += "<path stroke=\"none\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"black\" mask=\"url(#shaddowMask)\" fill-opacity=\"0.5\" "
    svgPath += "d=\""
    
    for(var i = 0; i < shaddow.size() ; i++)
    {
        if(i==0)
        {
            svgPath += "M" + (shaddow.get(i).getX()/100) + " " + (shaddow.get(i).getY()/100)
        }
        else
        {
            svgPath += " L" + (shaddow.get(i).getX()/100) + " " + (shaddow.get(i).getY()/100)
        }
    }
    svgPath += "\" />\n"
    // =====================================================================================================================================
    // Schatten nur wenn Sonne da ist
    // =====================================================================================================================================
    val ele = Double.parseDouble(SunElevation.state.toString.substring(0, 5))
    var tmpFarbe = PRIMARY_COLOR

    if(ele > 0)
    {
        svg += svgPath
        tmpFarbe = LIGHT_COLOR
    }
    // =====================================================================================================================================
    // Beleuchtete Seite einfärben
    // =====================================================================================================================================
    svgPath = "<path stroke=\"" + tmpFarbe + "\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"none\" d=\""
    
    for(var i = 0; i < side1.size() ; i++)
    {
        if(i==0)
        {
            svgPath += "M" + (side1.get(i).getX()/100) + " " + (side1.get(i).getY()/100)
        }
        else
        {
            svgPath += " L" + (side1.get(i).getX()/100) + " " + (side1.get(i).getY()/100)
        }
    }
    svgPath += "\" />\n"    
    svg += svgPath
    // =====================================================================================================================================
    // Haus Umriss
    // =====================================================================================================================================
    svgPath = "<path stroke=\"none\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"" + PRIMARY_COLOR + "\" d=\""
    
    for(var i = 0; i < SHAPE.size() ; i++)
    {
        if(i==0)
        {
            svgPath += "M" + (SHAPE.get(i).getX()/100) + " " + (SHAPE.get(i).getY()/100)
        }
        else
        {
            svgPath += " L" + (SHAPE.get(i).getX()/100) + " " + (SHAPE.get(i).getY()/100)
        }
    }

    svgPath += "\" />\n"
    svg += svgPath    
    // =====================================================================================================================================
    // Bogen "Nacht" erzeugen
    // =====================================================================================================================================
    var arch = ""
    var anfang = Double.parseDouble(SunsetPositionAzimuth.state.toString.substring(0, 5))
    var ende = Double.parseDouble(SunrisePositionAzimuth.state.toString.substring(0, 5))
    var bogenlaenge = ende - anfang

    if(bogenlaenge < 0)
    {
        bogenlaenge += 360
    }

    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - anfang)) * (WIDTH / 2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - anfang)) * (WIDTH / 2)

    arch += "<path d=\"M" + tmpX + " " + tmpY + " " 
    arch += "A" + (WIDTH / 2) + " " + (WIDTH / 2) + " 0 "

    if(bogenlaenge < 180)
    {
        arch += "0 1 "
    }
    else
    {
        arch += "1 1 "
    }
    
    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - ende)) * (WIDTH / 2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - ende)) * (WIDTH / 2)

    arch += tmpX + " " + tmpY + "\" stroke=\"" + PRIMARY_COLOR + "\""
    arch += " stroke-width=\"" + STROKE_WIDTH + "\" fill=\"none\" />\n"

    svg += arch
    // =====================================================================================================================================
    // Bogen "Tag" erzeugen
    // =====================================================================================================================================
    arch = ""
    anfang = Double.parseDouble(SunrisePositionAzimuth.state.toString.substring(0, 5))
    ende = Double.parseDouble(SunsetPositionAzimuth.state.toString.substring(0, 5))
    bogenlaenge = ende - anfang

    if(bogenlaenge < 0)
    {
        bogenlaenge += 360
    }

    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - anfang)) * (WIDTH / 2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - anfang)) * (WIDTH / 2)

    arch += "<path d=\"M" + tmpX + " " + tmpY + " " 
    arch += "A" + (WIDTH / 2) + " " + (WIDTH / 2) + " 0 "

    if(bogenlaenge < 180)
    {
        arch += "0 1 "
    }
    else
    {
        arch += "1 1 "
    }
    
    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - ende)) * (WIDTH / 2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - ende)) * (WIDTH / 2)

    arch += tmpX + " " + tmpY + "\" stroke=\"" + LIGHT_COLOR + "\""
    arch += " stroke-width=\"" + STROKE_WIDTH + "\" fill=\"none\" />\n"

    svg += arch
    // =====================================================================================================================================
    // Markierung Sonnenaufgang
    // =====================================================================================================================================
    svgPath += "<path stroke=\"" + LIGHT_COLOR +"\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"none\" d=\""
    
    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(SunrisePositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2-2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(SunrisePositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2-2)
    
    svgPath += "M" + tmpX + " " + tmpY

    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(SunrisePositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2+2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(SunrisePositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2+2)

    svgPath += " L" + tmpX + " " + tmpY +"\" />\n"
        
    svg += svgPath    
    // =====================================================================================================================================
    // Markierung Sonnenuntergang
    // =====================================================================================================================================
    svgPath = "<path stroke=\"" + LIGHT_COLOR +"\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"none\" d=\""
    
    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(SunsetPositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2-2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(SunsetPositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2-2)
    
    svgPath += "M" + tmpX + " " + tmpY

    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(SunsetPositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2+2)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(SunsetPositionAzimuth.state.toString.substring(0, 5)))) * (WIDTH / 2+2)

    svgPath += " L" + tmpX + " " + tmpY +"\" />\n"    
    svg += svgPath  
    // =====================================================================================================================================
    // Stundenkreis 
    // =====================================================================================================================================
    var abschnitt = newArrayList()
    abschnitt.add(Azimuth0.state.toString.substring(0, 5))
    abschnitt.add(Azimuth1.state.toString.substring(0, 5))
    abschnitt.add(Azimuth2.state.toString.substring(0, 5))
    abschnitt.add(Azimuth3.state.toString.substring(0, 5))
    abschnitt.add(Azimuth4.state.toString.substring(0, 5))
    abschnitt.add(Azimuth5.state.toString.substring(0, 5))
    abschnitt.add(Azimuth6.state.toString.substring(0, 5))
    abschnitt.add(Azimuth7.state.toString.substring(0, 5))
    abschnitt.add(Azimuth8.state.toString.substring(0, 5))
    abschnitt.add(Azimuth9.state.toString.substring(0, 5))
    abschnitt.add(Azimuth10.state.toString.substring(0, 5))
    abschnitt.add(Azimuth11.state.toString.substring(0, 5))
    abschnitt.add(Azimuth12.state.toString.substring(0, 5))
    abschnitt.add(Azimuth13.state.toString.substring(0, 5))
    abschnitt.add(Azimuth14.state.toString.substring(0, 5))
    abschnitt.add(Azimuth15.state.toString.substring(0, 5))
    abschnitt.add(Azimuth16.state.toString.substring(0, 5))
    abschnitt.add(Azimuth17.state.toString.substring(0, 5))
    abschnitt.add(Azimuth18.state.toString.substring(0, 5))
    abschnitt.add(Azimuth19.state.toString.substring(0, 5))
    abschnitt.add(Azimuth20.state.toString.substring(0, 5))
    abschnitt.add(Azimuth21.state.toString.substring(0, 5))
    abschnitt.add(Azimuth22.state.toString.substring(0, 5))
    abschnitt.add(Azimuth23.state.toString.substring(0, 5))

    var j = 0
    for(var i = 0; i < abschnitt.size() ; i++)
    {
        if(i == (abschnitt.size() -1))
        {
            j = 0
        }
        else
        {
            j = i +1
        }
        if(i % 2 == 0)
        {
            arch = ""
            anfang = Double.parseDouble(abschnitt.get(i))
            ende = Double.parseDouble(abschnitt.get(j))
            bogenlaenge = ende - anfang

            if(bogenlaenge < 0)
            {
                bogenlaenge += 360
            }

            tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - anfang)) * (WIDTH / 2+8)
            tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - anfang)) * (WIDTH / 2+8)

            arch += "<path d=\"M" + tmpX + " " + tmpY + " " 
            arch += "A" + (WIDTH / 2) + " " + (WIDTH / 2) + " 0 "

            if(bogenlaenge < 180)
            {
                arch += "0 1 "
            }
            else
            {
                arch += "1 1 "
            }
            
            tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - ende)) * (WIDTH / 2+8)
            tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - ende)) * (WIDTH / 2+8)

            arch += tmpX + " " + tmpY + "\" stroke=\"" + PRIMARY_COLOR + "\""
            arch += " stroke-width=\"3\" fill=\"none\" stroke-opacity=\"0.3\"" + " />\n"

            svg += arch
        }
        else
        {
            arch = ""
            anfang = Double.parseDouble(abschnitt.get(i))
            ende = Double.parseDouble(abschnitt.get(j))
            bogenlaenge = ende - anfang

            if(bogenlaenge < 0)
            {
                bogenlaenge += 360
            }

            tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - anfang)) * (WIDTH / 2+8)
            tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - anfang)) * (WIDTH / 2+8)

            arch += "<path d=\"M" + tmpX + " " + tmpY + " " 
            arch += "A" + (WIDTH / 2) + " " + (WIDTH / 2) + " 0 "

            if(bogenlaenge < 180)
            {
                arch += "0 1 "
            }
            else
            {
                arch += "1 1 "
            }
            
            tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - ende)) * (WIDTH / 2+8)
            tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - ende)) * (WIDTH / 2+8)

            arch += tmpX + " " + tmpY + "\" stroke=\"" + PRIMARY_COLOR + "\""
            arch += " stroke-width=\"3\" fill=\"none\" " + " />\n"

            svg += arch
        }
    }
    // =====================================================================================================================================
    // Markierung Mitternacht
    // =====================================================================================================================================
    svgPath = "<path stroke=\"" + LIGHT_COLOR +"\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"none\" d=\""
    
    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(Azimuth0.state.toString.substring(0, 5)))) * (WIDTH / 2+5)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(Azimuth0.state.toString.substring(0, 5)))) * (WIDTH / 2+5)
    
    svgPath += "M" + tmpX + " " + tmpY

    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(Azimuth0.state.toString.substring(0, 5)))) * (WIDTH / 2+11)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(Azimuth0.state.toString.substring(0, 5)))) * (WIDTH / 2+11)

    svgPath += " L" + tmpX + " " + tmpY + "\" />\n"

    svg += svgPath  
    // =====================================================================================================================================
    // Markierung Mittag
    // =====================================================================================================================================
    svgPath = "<path stroke=\"" + LIGHT_COLOR +"\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"none\" d=\""
    
    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(Azimuth12.state.toString.substring(0, 5)))) * (WIDTH / 2+5)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(Azimuth12.state.toString.substring(0, 5)))) * (WIDTH / 2+5)
    
    svgPath += "M" + tmpX + " " + tmpY

    tmpX = (WIDTH / 2) + Math.sin(Math.toRadians(180 - Double.parseDouble(Azimuth12.state.toString.substring(0, 5)))) * (WIDTH / 2+11)
    tmpY = (HEIGHT / 2) + Math.cos(Math.toRadians(180 - Double.parseDouble(Azimuth12.state.toString.substring(0, 5)))) * (WIDTH / 2+11)

    svgPath += " L" + tmpX + " " + tmpY + "\" />\n"
    svg += svgPath  
    // =====================================================================================================================================
    // Sonnenposition
    // =====================================================================================================================================
    svg += "<circle cx=\"" + sunX + "\" cy=\"" + sunY + "\" r=\"3\" stroke=\"" + LIGHT_COLOR + "\" stroke-width=\"" + STROKE_WIDTH + "\" fill=\"" + LIGHT_COLOR + "\" />\n"

    val datei = new File("/etc/openhab2/html/shadow.svg")
    val dateiwriter = new FileWriter(datei, false)

    dateiwriter.write(doctype + header + svg + footer)

    dateiwriter.flush()
    dateiwriter.close()

    UpdateSVG.sendCommand(OFF)
end

since im new, i can only add one upload per post, so here is my svg

shaddow_example

1 Like

Hello, has someone managed to run the script under OH3?

because it is python, it will run in OH3 without any issues… as it is calculated outside of OHx anyways…

Yes, it works, but a few adjustments need to be made.

Thanks to the original author for this script, it has been on all my tablets for many years now and is appreciated by the whole family.

I wonder if there is any licence to be put on this? I have included my modified copy (Python3, blackening etc) on pyrotun/houseshadow.py at master · berland/pyrotun · GitHub

I put GPLv3 on this github reposity, but would gladly correct it for this particular file according to the primary authors wishes.

For feature requests, I would like to display the rise and dawn times in a small font next to the rise and dawn tickmarks. Will try and see if I can get my head around the svg code.

@berland

I think the original license was the MIT License according to the GitHub repository where the original author published the script.

pmpkk/openhab-habpanel-theme-matrix

Another fork

Many thanks to the original author for this insane script.
A very cool nice-to-have on my HABPanel.

Changes in this fork

  • Python 3 compatible
  • using the python-openhab library
  • using InfluxDB v2‘s FLUX language with the official influxdb-client library
  • showing the current position of the moon

Download

The script and the How-To-Use can be found here:
GitHub: florian-h05/openhab-conf

Hello,
I am using this script over years with my OH 3 instance.
Now I have moved to a new system and I am struggeling to get the script running again.
I am faced with the following error messages:

PI4 with Raspberry PI OS (with debian bookworm).
Can somebody help?

Thanks,
Tobi

Which one do you use? there are several forks of the script.

Post #257 march 2020
Script und guidelines posted by kriznik.

Well, as the original script is only 379 lines long (and python complains about line 383…)
and in the original script there is a line 66 with

        for i in range(0, 24, HOURS):

I guess you throw some text in the file accidentally.

@togi
here is my script iteration (fairly same as in the post you mentioned but with wind direction as well and few little details), running it for years (OH4 now)
only slight issue with OH4 and basic sitemap UI is it’s not always refreshing self on UI for some reason, but svg is up to date correctly

about you issue:

xrange

is not even in the script, so you have typo there.

anyway make sure you have correctly defined SHAPE and you have these:

astral==1.10.1
pylunar

I was not rewriting it to the latest astral, as this just works

calling the script via rule:

rule "Updater: python: Shadow SVG"
when
    Item Sun_Azimuth received update or
    Item WS_WindAngle received update or
    System started
then
    executeCommandLine("python3", "/etc/openhab/scripts/shadow.py", "update", (WS_WindAngle.state as Number).floatValue.toString)
// debug
/*   
    val resp = executeCommandLine(Duration.ofSeconds(2), "python3", "/etc/openhab/scripts/shadow.py", "update", (WS_WindAngle.state as Number).floatValue.toString)
    logInfo("Shaddow", "Updating Shaddow SVG")          
    logInfo("Shaddow", resp) 
    logInfo("Shaddow", (WS_WindAngle.state as Number).floatValue.toString)
*/    
end

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

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

MOON_COLOR = '#999999'
MOON_RADIUS = 3
STROKE_WIDTH = '1'
FILENAME = '/etc/openhab/html/shadow.svg'

LATITUDE = 23.555555
LONGITUDE = 23.7777777
ALTITUDE = 371.0
TIMEZONE = 'Europe/Prague'
TOWN = 'Prague'

# 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}, \
		{'x': 36.16, 'y': 28.41}, \
        {'x': 35.51, 'y': 27.74}, \
		{'x': 35.63, 'y': 15.48}, \
		{'x': 46.14, 'y':  9.63}, \
		{'x': 56.50, 'y': 16.13}, \
		{'x': 56.63, 'y': 22.21}, \
		{'x': 69.77, 'y': 28.99}, \
		{'x': 69.99, 'y': 28.78}, \
		{'x': 85.67, 'y': 37.63}, \
		{'x': 67.10, 'y': 70.33}, \
		{'x': 66.09, 'y': 70.21}, \
		{'x': 58.27, 'y': 83.97}, \
		{'x': 39.55, 'y': 73.58}, \
		{'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())
		#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 range(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(LATITUDE)

		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,width=None):

		swith = STROKE_WIDTH
		if (width != None): swith = width

		p = ''
		p = p + '<path stroke="' + stroke + '" stroke-width="' + swith + '" 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, wind_azimuth=0):

		wind_azimuth = float(wind_azimuth)

		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)
		wind_pos = self.degreesToPoint(wind_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 + '" />'

		#wind
		svg = svg + self.generatePath(WIND_COLOR,'none',[self.degreesToPoint(wind_azimuth,WIDTH//2+5), self.degreesToPoint(wind_azimuth,WIDTH//2+20)],"","2")


		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

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

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

if __name__ == '__main__':
	main()

Thank you very much.
Script is working again, and BTW, the addtional wind direction indicator is cool.

Because of BasicUI has not been automatically updated with webview anymore, I found out how to do it with Image element instead.

All you need is to update script and install few thing

apt update
apt install libcairo2 -y
pip install cairosvg

then adjust script little bit

import cairosvg
....
FILENAME = '/etc/openhab/html/shadow.svg'
FILEPNG = '/etc/openhab/html/shadow.png'
....
....
....
		if(args[1] == "update"):
			s.generateSVG(args[2])
			cairosvg.svg2png(url=FILENAME,write_to=FILEPNG,scale=1.2)

then in sitemap file change

Webview url="/static/shadow.svg?{{itemValue('Sun_Randomizer')}}" icon="none"

to

Image url="https://<url of the openhab>/static/shadow.png" refresh=60000

and you’ve got refreshing image.

note: I can see some imperfections in the png which are not in the svg, so might try different library in the future.

well I found out why

Image url="https://<url of the openhab>/static/shadow.svg" refresh=60000

was not working correctly, so you can fix the script instead and no need to generate png from svg :wink:

change this:

		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">'

to this

		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" width="150px" height="150px">'

Hi,
could this feature be embedded into the OH, to have it working natively as a feature of Main UI pages, lets say Home Plan page…

or, can we use something like https://app.shadowmap.org/ or https://shademap.app/ or something similar instead of openstreetmap when displaying a house on the map?

I see some built in options to select providers of overlay and background tiles, but couldn’t get them to work…

1 Like