|
[{"id":"a1385ae3163e5fb7","type":"group","z":"088f35b28bce9017","name":"Forecast.solar by API calls","style":{"stroke":"#ffC000","fill":"#ffefbf","label":true,"color":"#6f2fa0"},"nodes":["cf337560acceb64a","a43e0a47447b4560","f527d89dbfa94eb4","54894a7516152677","501a74354a85c941","df8d08051939900c","55f76ba1eb4d240c","ea3720e2536f05bd","4afb9d89b8e36784","f0a07ec90a4950c4","2806715089552e3f","26f90a9d2ea7881c","99bbb40e2951f583","4685904a1b395f0f","6cd59bc71d7d0561","2fcf678ad93ceded","a1657abd3f7fbe07","021dfe3c33b03bd0","964720b6b93ae8d8","300932bb94e9b844","f99731c4c931d09d","b53660619b928eca","14f2e37ab17a5a12","54df8bc58f816a0d","942640410dc2f972","6106d81f794fb33a","f2750452e83cd50b","f234592fc9e52765","ca6144d02b26d86c","2bd2cf5d9a81ea51","52280e7a812a04ab","2bb2e151e4ed1070","256c4a44a625de26","cbaa815ce61e7a73","7d8c556d0ee9de53","5b81da834c65a49a","8c59a5397a90c88f","3434ef7b794bc505","0bb03e96583cf962","f36f3e3e1b923c00","ec32508e339d7262","42be2d7b12872b97","a10cf95229654d4b","fcc95a7b1ccc30fa","70cf7269c18b02c7","af6e2c8fa93af511","46273418991a9fa0"],"x":94,"y":79,"w":1012,"h":702},{"id":"cf337560acceb64a","type":"ui_chart","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"Forecast","group":"76e876790e22c53e","order":2,"width":0,"height":0,"label":"SolarForecast (Yesterday-Today-Tomorrow)","chartType":"line","legend":"true","xformat":"HH:mm","interpolate":"cubic","nodata":"","dot":true,"ymin":"","ymax":"","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#d62728","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":420,"y":740,"wires":[[]]},{"id":"a43e0a47447b4560","type":"ui_text","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","group":"76e876790e22c53e","order":3,"width":5,"height":1,"name":"","label":"Energy Today","format":"{{msg.payload}} kWh","layout":"row-spread","className":"","x":440,"y":660,"wires":[]},{"id":"f527d89dbfa94eb4","type":"ui_text","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","group":"76e876790e22c53e","order":5,"width":4,"height":1,"name":"","label":"Date","format":"{{msg.payload.dates.today}}","layout":"row-spread","className":"","x":250,"y":620,"wires":[]},{"id":"54894a7516152677","type":"ui_text","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","group":"76e876790e22c53e","order":7,"width":5,"height":1,"name":"","label":"Energy Tomorrow","format":"{{msg.payload}} kWh","layout":"row-spread","className":"","x":450,"y":700,"wires":[]},{"id":"501a74354a85c941","type":"ui_text","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","group":"76e876790e22c53e","order":9,"width":4,"height":1,"name":"","label":"Period","format":"{{msg.payload}}","layout":"row-spread","className":"","x":410,"y":540,"wires":[]},{"id":"df8d08051939900c","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Period","rules":[{"t":"set","p":"payload","pt":"msg","to":"$substring(payload.today.start,11,5) & \" to \" & $substring(payload.today.stop,11,5)\t","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":540,"wires":[["501a74354a85c941"]]},{"id":"55f76ba1eb4d240c","type":"ui_text","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","group":"76e876790e22c53e","order":6,"width":5,"height":1,"name":"","label":"P1 update","format":"{{msg.payload.P1.time}}","layout":"row-spread","className":"","x":430,"y":580,"wires":[]},{"id":"ea3720e2536f05bd","type":"ui_text","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","group":"76e876790e22c53e","order":10,"width":5,"height":1,"name":"","label":"P2 update","format":"{{msg.payload.P2.time}}","layout":"row-spread","className":"","x":430,"y":620,"wires":[]},{"id":"4afb9d89b8e36784","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"lastRead","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.lastRead","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":580,"wires":[["ea3720e2536f05bd","55f76ba1eb4d240c"]]},{"id":"f0a07ec90a4950c4","type":"comment","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Rev: May 2023","info":"Complete re-write using JSONata\n- improve plane processing so can add planes more easily\n- improve forecast analysis with extra fields\n- duplicate analysis for tomorrow as well as today\n- tidy date calculations: now use time(local) and time_utc from\n API return to calculate timezone offset and then all dates from that\n- remove all use of moment node (get 'now' using timezone offset)\n- fix bug with actual hour at new day\n- move context read & write to change nodes (easier to select context store)\n\nThis has been well tested but may still raise\nissues from the use of JSONata\nTimezone has been tested for UK (BST = GMT+1)\n\nThere is no error handling where the API call fails, the\nforecast will just not update at that hour. The actual/history\nwill still continue to process however.\n","x":540,"y":120,"wires":[]},{"id":"2806715089552e3f","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Site","rules":[{"t":"set","p":"parm.latitude","pt":"msg","to":"51.3","tot":"num"},{"t":"set","p":"parm.longitude","pt":"msg","to":"0.9","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":190,"y":180,"wires":[["26f90a9d2ea7881c","4685904a1b395f0f"]]},{"id":"26f90a9d2ea7881c","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"P1 East","rules":[{"t":"set","p":"parm.plane","pt":"msg","to":"1","tot":"num"},{"t":"set","p":"parm.elevation","pt":"msg","to":"35","tot":"num"},{"t":"set","p":"parm.azimuth","pt":"msg","to":"-105","tot":"num"},{"t":"set","p":"parm.power","pt":"msg","to":"2.19","tot":"num"},{"t":"set","p":"payload","pt":"msg","to":"{\"damping_morning\":0.4,\"damping_evening\":0.1,\"horizon\":\"20,24,28,32,36,40,44,40,36,32,28,16,16,16,16,16,10,16,16,16,19,19,19,4,4,4,4,16,22,16,16,16,16,16,5,5,5,9,9,9,9,9,18,18,18,17,17,17,6,6,12,11,11,7,10,10,6,6,6,6,6,6,6,6,6,6,6,10,12,14,16,18\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":180,"wires":[["99bbb40e2951f583"]]},{"id":"99bbb40e2951f583","type":"http request","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"API FCS","method":"GET","ret":"obj","paytoqs":"query","url":"https://api.forecast.solar/estimate/{{{parm.latitude}}}/{{{parm.longitude}}}/{{{parm.elevation}}}/{{{parm.azimuth}}}/{{{parm.power}}}","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":520,"y":180,"wires":[["2fcf678ad93ceded"]]},{"id":"4685904a1b395f0f","type":"delay","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"10s","pauseType":"delay","timeout":"10","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":190,"y":220,"wires":[["6cd59bc71d7d0561","300932bb94e9b844"]]},{"id":"6cd59bc71d7d0561","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"P2 West","rules":[{"t":"set","p":"parm.plane","pt":"msg","to":"2","tot":"num"},{"t":"set","p":"parm.elevation","pt":"msg","to":"35","tot":"num"},{"t":"set","p":"parm.azimuth","pt":"msg","to":"75","tot":"num"},{"t":"set","p":"parm.power","pt":"msg","to":"2.19","tot":"num"},{"t":"set","p":"payload","pt":"msg","to":"{\"damping_morning\":0.1,\"damping_evening\":0.3,\"horizon\":\"20,24,28,32,36,40,44,40,36,32,28,16,16,16,16,16,10,16,16,16,19,19,19,4,4,4,4,16,22,16,16,16,16,16,5,5,5,9,9,9,9,9,18,18,18,17,17,17,6,6,12,11,11,7,10,10,6,6,6,6,6,6,6,6,6,6,6,10,12,14,16,18\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":220,"wires":[["99bbb40e2951f583"]]},{"id":"2fcf678ad93ceded","type":"switch","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"OK?","property":"payload.message.code","propertyType":"msg","rules":[{"t":"eq","v":"0","vt":"num"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":650,"y":180,"wires":[["a1657abd3f7fbe07"],[]]},{"id":"a1657abd3f7fbe07","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Dates / Array / Plane","rules":[{"t":"set","p":"payload.dates","pt":"msg","to":"(\t /* FUNCTIONS */\t\t $getdate:=function($ts){$substringBefore($ts,\"T\")};\t $moveday:=function($ts,$by){$fromMillis($toMillis($ts)+$by*86400000)};\t\t /* using returned local and utc timestamps, extract local timezone offset */\t /* note- this is based on solar coordinates given in API call */\t /* if machine is on a different timezone $utclocal will not be correct */\t \t $solarlocal:=payload.message.info.time;\t $solarutc:=payload.message.info.time_utc;\t $timezone:=$substring($solarlocal,19);\t $tssolar:=$substring($solarlocal,0,19);\t $tsutc:=$substring($solarutc,0,19);\t $mssolar:=$toMillis($tssolar)-$toMillis($tsutc);\t\t /* tidy today (timestamp, date) as local time */ \t \t $plainnow:=$replace($tssolar,\"T\",\" \");\t $today:= $getdate($tssolar);\t\t /* get equivalent UTC timestamp adjusted for timezone shift for date (+/- 1 day) calculation */\t /* eg - local 00:10 (+3), UTC 21:10 (yesterday) :: utclocal = 21:10 +3:00 => 00:10 (today) */\t /* - local 00:10 (-3), UTC 03:10 (today) :: utclocal = 03:10 -3:00 => 00:10 (today) */\t \t $utclocal:=$fromMillis($toMillis($tssolar)+$mssolar);\t \t $dates:={\t \"yesterday\": $getdate($moveday($utclocal,-1)),\t \"today\": $today,\t \"tomorrow\": $getdate($moveday($utclocal,1)),\t \"lastApiCall\":$plainnow,\t \"timezone\": $timezone,\t \"msoffset\": $mssolar,\t \"tsDateChange\": $solarlocal \t } \t)","tot":"jsonata"},{"t":"set","p":"payload.dates.fcDateToday","pt":"msg","to":"$keys(payload.result.watt_hours_day)[0]","tot":"jsonata"},{"t":"set","p":"sf","pt":"msg","to":"SolarFC","tot":"flow","dc":true},{"t":"set","p":"SolarFC","pt":"flow","to":"(\t/* April 2023 */\t/* take incoming API from forecast solar and process */\t/* read & write to flow context in change node to */\t/* make it easier to modify storage context settings */\t\t/* FUNCTION: to create a new empty solar table */\t $setTable:=function(){(\t $dtom:=payload.dates.tomorrow;\t $dday:=payload.dates.today;\t $dyes:=payload.dates.yesterday;\t $arr:=[0..71];\t $arr#$i.(\t $ts:= $i<24 ? $dyes : $i<48 ? $dday : $dtom;\t $ts:= $ts & \" \" & $pad($string($i%24),-2,\"0\") & \":00:00\";\t {\"hour\":$i%24,\t \"timestamp\": $ts,\t \"actualWh\": null,\t \"efcWh\": null,\t \"fcW\": null, /* keep as null, issue at start of day when update moves '0' to oldfcw */\t \"oldfcW\": null}\t )\t )};\t\t/* read solar forecast from context or create new array */\t/* if forecast_today after tomorrow, start a fresh array */\t/* if forecast_today = tomorrow, shift array 1 day left */\t/* and reload all dates or just update 'last api call' */\t $sf:= sf;\t $mt:= {\"energy\": 0, \"start\": null, \"stop\": null};\t $not($exists($sf)) ? $sf:={\t \"solarTable\": $setTable(),\t \"today\": $mt,\t \"tomorrow\": $mt,\t \"dates\": payload.dates,\t \"lastRead\": {\"P1\": {\"time\": null, \"watts\": null, \"todayTotal\": null, \"tomorrowTotal\": null}}\t };\t $tabTom:=$sf.dates.tomorrow;\t $fcToday:=payload.dates.fcDateToday;\t $fcToday>$tabTom ? $sf:= $sf ~> | $ | {\"solarTable\": $setTable()} |;\t $fcToday=$tabTom ? $sf:= $sf ~> | $ | {\"solarTable\": $append($sf.solarTable[[24..71]], $setTable()[[48..71]])} |;\t $fcToday>=$tabTom ? $sf:= $sf ~> | $ | {\"dates\": $$.payload.dates} | : $sf:= $sf ~> | dates | {\"lastApiCall\": $$.payload.dates.lastApiCall} |;\t\t/* add in plane results to 'lastRead' in context */\t $p:={\"P\" & parm.plane: {\"time\": payload.dates.lastApiCall,\t \"watts\": payload.result.watts,\t \"todayTotal\": $lookup(payload.result.watt_hours_day,payload.dates.today),\t \"tomorrowTotal\": $lookup(payload.result.watt_hours_day,payload.dates.tomorrow)}};\t $sf ~> | lastRead | $p |; \t)\t","tot":"jsonata"},{"t":"set","p":"payload.power","pt":"msg","to":" /* lookup the current power (watts) using the current hour */\t (\t $hour:=$substringBefore(payload.dates.lastApiCall,\":\") & \":00:00\";\t $power:=$lookup(payload.result.watts,$hour);\t $exists($power) ? $power : 0\t )","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":820,"y":180,"wires":[["14f2e37ab17a5a12","af6e2c8fa93af511"]]},{"id":"021dfe3c33b03bd0","type":"api-current-state","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"Solar Act Wh hr30","server":"","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"sensor.solar_energy_hr30","state_type":"num","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entity"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":370,"y":360,"wires":[["46273418991a9fa0"]]},{"id":"964720b6b93ae8d8","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Update SFC (act & fc)","rules":[{"t":"set","p":"sf","pt":"msg","to":"SolarFC","tot":"flow","dc":true},{"t":"set","p":"actual.index","pt":"msg","to":"$parseInteger(actual.hour, \"99\")+23\t","tot":"jsonata"},{"t":"set","p":"actual.value","pt":"msg","to":"$number(payload.attributes.last_period)*1000","tot":"jsonata"},{"t":"set","p":"payload","pt":"msg","to":"(\t/* May 2023 */\t\t/* FUNCTION: extract total seconds from string time as hh:mm:ss => ss+(mm+hh*60)*60 */\t $tosec:=function($ts){(\t $a:=$substringAfter($ts,\" \");\t $h:=$substringBefore($a,\":\").$number();\t $b:=$substringAfter($a,\":\");\t $m:=$substringBefore($b,\":\").$number();\t $s:=$substringAfter($b,\":\").$number();\t $number($s+60*($m+60*$h))\t )};\t\t/* FUNCTION: get start or stop times for today or tomorrow. Uses $detail */\t/* in some cases $detail[value=0, date, am/pm] returns an array so assume array and take first */\t/* or last element. This happens when last hour value is 0, eg at 20:00 and at 20:01 sunset =0 */\t $timeis:=function($day, $part){(\t $a:=$detail[value=0 and date=($day=\"today\" ? $today : $tomrw)];\t $b:=$part=\"am\" ? ($a[hour<12].key)[0] : ($a[hour>12].key)[-1];\t )};\t\t/* MAIN code */\t\t $oldsf:=sf;\t $today:=sf.dates.today;\t $tomrw:=sf.dates.tomorrow;\t\t/* save actual (index = last hour hh) this should be energy Wh between hh-:30 and hh+:30 */\t/* also save pre-update forecast value for the last hour to forecast history */\t $index:=actual.index;\t $value:=actual.value;\t $fcw:=$oldsf.solarTable[$index].fcW;\t $sf:= $oldsf ~> | solarTable[$index] | {\"actualWh\": $value, \"oldfcW\": $fcw} |;\t\t/* reset all forecasts (today/tomorrow) to zero, collate forecast watts into detail array */\t $sf:= $sf ~> | solarTable[[24..71]] | {\"fcW\": 0} |;\t\t $watts:=$sf.lastRead.*.watts;\t $detail:=$keys($watts).{\"key\": $,\t \"date\": $.$substringBefore(\" \"),\t \"time\": $.$substring(11,5),\t \"hour\": $number($substring($,11,2)),\t \"value\": $sum($lookup($watts, $))};\t\t/* set start & end times and total energy for today & tomorrow, with delta */\t/* set seconds difference, where +ve is longer day (ealier start/later end) */\t\t $start1:=$timeis(\"today\", \"am\");\t $stop1:= $timeis(\"today\", \"pm\");\t $start2:=$timeis(\"tomrw\", \"am\");\t $stop2:= $timeis(\"tomrw\", \"pm\");\t $obj1:= {\"today\": {\"energy\": $sum($sf.lastRead.*.todayTotal), \"start\": $start1, \"stop\": $stop1}};\t $obj2:= {\"tomorrow\": {\"energy\": $sum($sf.lastRead.*.tomorrowTotal), \"start\": $start2, \"stop\": $stop2,\t \"startdelta\": $tosec($start1)-$tosec($start2), \"stopdelta\": $tosec($stop2)-$tosec($stop1)}};\t\t $sf:= $sf ~> | $ | $obj1 |;\t $sf:= $sf ~> | $ | $obj2 |;\t\t/* add in index reference based on hour - used to update main solar table */\t $detail:= $detail ~> | $ | {\"index\": value>0 ? date=$today ? 24+hour : 48+hour} |;\t\t/* (map over solar table; where detail/table index match, update 'fcW', then replace entire table) */ \t $stable:=$map($sf.solarTable, function($v, $i){$merge([ $v, {\"fcW\": $detail[index=$i].value}])});\t $sf:= $merge($append($spread($sf),{\"solarTable\": $stable})); \t\t/* recalculate watt-hours for each hr:-30 to hr:+30 and post to hh:00 */\t/* using (a+b+b+c)/4, run from $index (last hour) to end of today */\t/* where oldfcW is not null (ie at start index-1) use that and not fcW */\t\t $stable:=$map($sf.solarTable, function($v, $i, $tab){(\t $i>=$index and $i<48 ? (\t $type($tab[$i-1].oldfcW)=\"number\" ? $a:= $tab[$i-1].oldfcW : $a:= $tab[$i-1].fcW;\t $b:=$tab[$i].fcW;\t $c:=$tab[$i+1].fcW;\t $merge([$v, {\"efcWh\": $floor(($a+2*$b+$c)/4)}]);\t ) : $v\t )}\t );\t $merge($append($spread($sf),{\"solarTable\": $stable}))\t)\t","tot":"jsonata"},{"t":"set","p":"SolarFC","pt":"flow","to":"payload","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":740,"y":360,"wires":[["6106d81f794fb33a","52280e7a812a04ab"]]},{"id":"300932bb94e9b844","type":"delay","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"10s","pauseType":"delay","timeout":"10","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":190,"y":300,"wires":[["021dfe3c33b03bd0"]]},{"id":"f99731c4c931d09d","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Read SFC","rules":[{"t":"set","p":"payload","pt":"msg","to":"SolarFC","tot":"flow","dc":true},{"t":"set","p":"actual.hour","pt":"msg","to":"$fromMillis($toMillis($now())+payload.dates.msoffset) ~> $substring(11,2)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":230,"y":500,"wires":[["fcc95a7b1ccc30fa"]]},{"id":"b53660619b928eca","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Chart Array","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t $table:=payload.solarTable;\t [{\"series\":[\"Forecast\", \"History\", \"Energy\", \"Actual\"],\t \"data\": [[$table.fcW], [$table.oldfcW], [$table.efcWh], [$table.actualWh]],\t \"labels\": [$table.(hour & \":00\")]}];\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":270,"y":740,"wires":[["cf337560acceb64a"]]},{"id":"14f2e37ab17a5a12","type":"switch","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"by plane","property":"parm.plane","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"num"},{"t":"eq","v":"2","vt":"num"}],"checkall":"false","repair":false,"outputs":2,"x":760,"y":240,"wires":[["f2750452e83cd50b"],["f234592fc9e52765"]]},{"id":"54df8bc58f816a0d","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"HA Graph Array","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t $table:=payload.solarTable;\t $index:=$parseInteger(actual.hour, \"99\")+24;\t\t {\"time\": [$table.timestamp],\t \"forecast\": [$table.fcW],\t \"energyfc\": [$table.efcWh],\t \"actual\": [$table.actualWh],\t \"old\": [$table.oldfcW],\t \"update\": $table[$index].timestamp}\t)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":700,"y":640,"wires":[["ca6144d02b26d86c"]]},{"id":"942640410dc2f972","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Analyse Forecast","rules":[{"t":"set","p":"payload","pt":"msg","to":"(\t/* April 2023. Node-RED JSONata to analyse solar forecast table into power levels and their associated time periods */\t\t/* FUNCTION: extract total minutes from string time as hh:mm => mm+hh*60 */\t $tomins:=function($ts){$number($substringBefore($ts,\":\"))*60 + $number($substringAfter($ts,\":\"))};\t\t/* FUNCTION: turn total minutes back to string hh:mm */\t $tohm:=function($mins){$pad($string(($mins-$mins%60)/60),-2, \"0\") & \":\" & $pad($string($mins%60),-2, \"0\")};\t\t/* FUNCTION: build power array @ $Pinc W intervals up to $Plim W, with forecast start & stop to record 0 (zero power) */\t $getpower:=function($day){(\t $daystart:= $day=\"today\" ? $substring(payload.today.start,11,5) : $substring(payload.tomorrow.start,11,5);\t $daystop:= $day=\"today\" ? $substring(payload.today.stop,11,5) : $substring(payload.tomorrow.stop,11,5);\t $pzero:=[{\"start\": $tomins($daystart), \"stop\": $tomins($daystop), \"minutes\": 0 }];\t ([0..$Pnum])#$i.{\"power\": $i*$Pinc, \"count\": 0, \"period\": ($i=0 ? $pzero: [])};\t )};\t\t/* FUNCTION: build detail array from each 1h period in solar day - start using an iteration over the solar table array for today */\t/* or tomorrow then add fields based on those previously added (this->last->solar/delta->event->offset, delta->peak/min/max) */\t $getdetail:=function($hrs, $daystart, $daystop){(\t $da:= payload.solarTable[$hrs]#$i.{\t \"index\": $i+24,\t \"hour\": hour,\t \"this\": $Pold ? ($type(oldfcW)=\"number\" ? oldfcW : fcW) : fcW};\t /* add 'last' by mapping over detail array using index & array to get offset value */\t $da:=$map($da, function($v, $i, $a){(\t $merge($append($v, {\"last\": $i>0 ? $a[$i-1].this : 0}));\t )});\t /* construct using tranform operator, as offset array values are not required */\t $da:= $da ~> |$| {\"solar\": this>0 or last>0 ? \"day\" : \"night\",\t \"delta\": this>last ? \"+\" : this<last ? \"-\" : \"=\"}|;\t $da:=$da ~> |$| {\"event\": solar=\"day\" ? last=0 ? \"start\" : \"run\" : \"off\"}|;\t $da:=$da ~> |$| solar=\"day\" and this=0 ? {\"event\": \"stop\"} |;\t /* minutes offset at solar start & end used for calculating power period times */\t $da:=$da ~> |$| {\"offset\": event=\"start\" ? $daystart%60 : event=\"stop\" ? 60-$daystop%60 : 0}|;\t /* compare trends to set maximum (+-) and minimum (-+) peaks in array using this and next value */\t $map($da, function($v, $i, $a){(\t $pair:=$i<$count($a)-1 ? $v.delta & $a[$i+1].delta;\t $merge($append($v, {\"peak\": $pair=\"+-\" ? \"max\" : $pair=\"-+\" ? \"min\" : \"-\"}));\t )}); \t )};\t\t/* FUNCTION: get power levels using the detail array */\t/* obtain array of power-level period start & end times. Between last & this value find */\t/* all Pinc Watt power levels, for each get pro-rata start (rise) & end (fall) times on */\t/* a linear basis. For first and last periods adjust times using start & end mins offset */\t $getpl:=function($det){(\t $pls:=$det[solar=\"day\"]#$i.(\t $last:=last;\t $this:=this;\t $offset:=offset;\t $hour:=hour;\t delta=\"+\" ? (\t $pstart:=$floor((last*$Pfac)/1000)+1;\t $pend:=$floor((this*$Pfac)/1000);\t $loop:=[$pstart..$pend];\t $loop.(\t $plindex:=$;\t $mins:=$floor((($plindex*$Pinc-$last)/($this-$last))*(60-$offset));\t {\"pl\": $plindex, \"power\": $plindex*$Pinc, \"start\": ($hour-1)*60+$mins+$offset};\t );\t ) : delta=\"-\" ? (\t $pstart:=$floor((last*$Pfac)/1000);\t $pend:=$floor((this*$Pfac)/1000)+1;\t $loop:=$reverse([$pend..$pstart]);\t $loop.(\t $plindex:=$;\t $mins:=$floor((($last-$plindex*$Pinc)/($last-$this))*(60-$offset));\t {\"pl\": $plindex, \"power\": $plindex*$Pinc, \"stop\": ($hour-1)*60+$mins};\t );\t ));\t /* sort by power (align starts/stops) then zip together and merge into one period object */\t /* for each power level, multiple periods for a single power level are in one array */\t $pls^(power)\t )};\t\t/* FUNCTION: zip power levels together and merege into one period object */\t /* for each power level, multiple periods for a single power level are in one array */\t $getperiods:=function(){\t $zip($plevels[start>0],$plevels[stop>0]).$merge($)\t };\t\t/* FUNCTION: iterate over power array, replace \"period\" with new period array where power levels match */\t /* use [] at end of period {} to force period object to be in array even for singletons */\t $update:=function($pwrarr, $prds){(\t $pa:=$map($pwrarr, function($v, $i, $a){(\t $period:=$prds[power=$v.power];\t $merge($append($v,{ \"count\": $i=0 ? 1 : $count($period),\t \"period\": $period.{\"start\": start, \"stop\": stop}[]\t }));\t )});\t\t /* tidy by setting duration minutes for each period and reverting start-stop to hh:mm */\t $pa ~> |period| {\"start\": $tohm(start), \"stop\": $tohm(stop), \"minutes\": stop-start} |;\t )};\t\t/* FUNCTION: build output object for the day, uses $powerarray, $detail, $plevels*/\t /* get minhour up front, if this does not exist default to [] */\t $dayis:=function($daydate){(\t $minhour:=$detail[peak=\"min\"].($tohm(hour*60))[];\t $minhour:= $exists($minhour) ? $minhour : [];\t {\"date\": $daydate,\t \"solardaystart\": $powerarray[0].period.start,\t \"solardayend\": $powerarray[0].period.stop,\t \"solardaymins\": $powerarray[0].period.minutes,\t \"fullhourstartat\": $tohm($detail[event=\"start\"].hour*60),\t \"fullhourendat\" : $tohm($detail[event=\"stop\"].(hour-1)*60),\t \"maxpower\": $max($detail.this),\t \"maxlevel\": $max($plevels.power),\t \"dayenergy\": $round($sum($detail.this)/100)/10,\t \"maxhour\": $detail[peak=\"max\"].($tohm(hour*60))[],\t \"minhour\": $exists($minhour) ? $minhour : [],\t \"powerarray\": $powerarray};\t )};\t\t\t\t/* SET PARAMERTERS */\t/* parameters: for setting power level array - increment and limit */\t/* increment should be integer factor of 1000 (25,40,50,100,125,200,250,500) */\t $Pinc:=100;\t $Plim:=4000;\t $Pnum:=$floor($Plim/$Pinc);\t $Pfac:=$floor(1000/$Pinc);\t\t/* parameters: for options */\t $Pold:=true; /* values: use old-forecast (history) where it exists, otherwise latest forecast */\t\t/* MAIN CODE */\t\t/* get power array, detail array, power levels, and power periods for today, build result object */\t $powerarray:=$getpower(\"today\");\t $detail:=$getdetail([24..47],$powerarray[0].period.start,$powerarray[0].period.stop);\t $plevels:=$getpl($detail);\t $periods:=$getperiods();\t $powerarray:=$update($powerarray, $periods);\t $todayis:=$dayis(payload.dates.today);\t\t/* repeat for tomorrow */\t\t $powerarray:=$getpower(\"tomorrow\");\t $detail:=$getdetail([48..71],$powerarray[0].period.start,$powerarray[0].period.stop);\t $plevels:=$getpl($detail);\t $periods:=$getperiods();\t $powerarray:=$update($powerarray, $periods);\t $tomorrowis:=$dayis(payload.dates.tomorrow);\t\t/* return final result object. note the updated is UTC and not local time */\t {\t \"today\": $todayis,\t \"tomorrow\": $tomorrowis,\t \"interval\": $Pinc,\t \"parmvalue\": $Pold ? \"oldvalue\" : \"forecast\",\t \"updated\": $toMillis($now())\t }\t)","tot":"jsonata"},{"t":"set","p":"SolarPT","pt":"flow","to":"payload","tot":"msg","dc":true}],"action":"","property":"","from":"","to":"","reg":false,"x":710,"y":460,"wires":[["2bd2cf5d9a81ea51","cbaa815ce61e7a73","5b81da834c65a49a"]]},{"id":"6106d81f794fb33a","type":"delay","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"5s","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":450,"y":460,"wires":[["fcc95a7b1ccc30fa"]]},{"id":"f2750452e83cd50b","type":"ha-sensor","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"Solar FC Power East","entityConfig":"025c69fc2ba0425c","version":0,"state":"payload.power","stateType":"msg","attributes":[],"inputOverride":"allow","outputProperties":[],"x":980,"y":240,"wires":[[]]},{"id":"f234592fc9e52765","type":"ha-sensor","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"Solar FC Power West","entityConfig":"4dba971878b0daa9","version":0,"state":"payload.power","stateType":"msg","attributes":[],"inputOverride":"allow","outputProperties":[],"x":980,"y":300,"wires":[[]]},{"id":"ca6144d02b26d86c","type":"ha-sensor","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"FC Solar Table","entityConfig":"62602840fda5176b","version":0,"state":"payload.update","stateType":"msg","attributes":[{"property":"FChours","value":"payload.time","valueType":"msg"},{"property":"FCwatts","value":"payload.forecast","valueType":"msg"},{"property":"FCactual","value":"payload.actual","valueType":"msg"},{"property":"FCold","value":"payload.old","valueType":"msg"},{"property":"FCwh","value":"payload.energyfc","valueType":"msg"}],"inputOverride":"block","outputProperties":[],"x":960,"y":640,"wires":[[]]},{"id":"2bd2cf5d9a81ea51","type":"ha-sensor","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"FC Estimate Today","entityConfig":"7fcde925c164fddb","version":0,"state":"payload.today.dayenergy","stateType":"msg","attributes":[{"property":"power","value":"payload.today.powerarray","valueType":"msg"},{"property":"start","value":"payload.today.solardaystart","valueType":"msg"},{"property":"stop","value":"payload.today.solardayend","valueType":"msg"},{"property":"maximum","value":"payload.today.maxhour","valueType":"msg"},{"property":"minimum","value":"payload.today.minhour","valueType":"msg"},{"property":"maxPower","value":"payload.today.maxpower","valueType":"msg"},{"property":"date","value":"payload.today.date","valueType":"msg"}],"inputOverride":"block","outputProperties":[],"x":970,"y":460,"wires":[[]]},{"id":"52280e7a812a04ab","type":"debug","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Debug Solar FC","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":960,"y":360,"wires":[]},{"id":"2bb2e151e4ed1070","type":"inject","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Manual","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":190,"y":360,"wires":[["021dfe3c33b03bd0"]]},{"id":"256c4a44a625de26","type":"cronplus","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"Every Hour @ 2mins past","outputField":"payload","timeZone":"","persistDynamic":false,"commandResponseMsgOutput":"output1","outputs":1,"options":[{"name":"schedule1","topic":"ForecastHourly","payloadType":"default","payload":"","expressionType":"cron","expression":"2 * * * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"}],"x":250,"y":120,"wires":[["2806715089552e3f"]]},{"id":"cbaa815ce61e7a73","type":"debug","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Debug Solar PT","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":960,"y":420,"wires":[]},{"id":"7d8c556d0ee9de53","type":"inject","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"Startup - refresh","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":"2","topic":"","payload":"","payloadType":"date","x":240,"y":460,"wires":[["f99731c4c931d09d"]]},{"id":"5b81da834c65a49a","type":"ha-sensor","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"FC Estimate Tomorrow","entityConfig":"eeac2705f3d9286b","version":0,"state":"payload.tomorrow.dayenergy","stateType":"msg","attributes":[{"property":"power","value":"payload.tomorrow.powerarray","valueType":"msg"},{"property":"start","value":"payload.tomorrow.solardaystart","valueType":"msg"},{"property":"stop","value":"payload.tomorrow.solardayend","valueType":"msg"},{"property":"maximum","value":"payload.tomorrow.maxhour","valueType":"msg"},{"property":"minimum","value":"payload.tomorrow.minhour","valueType":"msg"},{"property":"maxpower","value":"payload.tomorrow.maxpower","valueType":"msg"},{"property":"date","value":"payload.tomorrow.date","valueType":"msg"}],"inputOverride":"allow","outputProperties":[],"x":980,"y":520,"wires":[[]]},{"id":"8c59a5397a90c88f","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"kWh","rules":[{"t":"set","p":"payload","pt":"msg","to":"$round(payload.today.energy/1000,1)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":660,"wires":[["a43e0a47447b4560"]]},{"id":"3434ef7b794bc505","type":"change","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"kWh","rules":[{"t":"set","p":"payload","pt":"msg","to":"$round(payload.tomorrow.energy/1000,1)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":250,"y":700,"wires":[["54894a7516152677"]]},{"id":"0bb03e96583cf962","type":"comment","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Actual energy","info":"Note\nActual reading is taken each hour (h) as the \nlast-reset period from (h-2):30 to (h-1):30\nThe last reset hour is used to access the\nsolar array table, effectively for the \nprevious hour.\nAt 01:mm update, last reset was 00:30 thus\nhour was 00, and index should be 24 (start\nof the current day in the array).\nAt 00:mm update, last reset was 23:30 from\nprior day, hour is 23, and index would be\n47, except that this is the start of a new\nday and the array has just been shifted left\nIndex 47 is now at index 23.\nTo address this, the hour node collects the\nhour value from the (last reset +1) hour, which\nmaps to 00 - 23. Then 23 is added to map from\nindex 23 to 46.\nThis sets the actual into the correct part of \nthe table array for the first hour of the new\nday (index 23).\nGlad I got that one fixed...","x":410,"y":320,"wires":[]},{"id":"f36f3e3e1b923c00","type":"comment","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Current hour ->\\n forecast power","info":"","x":760,"y":280,"wires":[]},{"id":"ec32508e339d7262","type":"comment","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Total energy\\n forecasts & table","info":"","x":960,"y":580,"wires":[]},{"id":"42be2d7b12872b97","type":"comment","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Forcast table for\\n Apex chart graph","info":"","x":960,"y":700,"wires":[]},{"id":"a10cf95229654d4b","type":"junction","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","x":140,"y":540,"wires":[["df8d08051939900c","4afb9d89b8e36784","f527d89dbfa94eb4","8c59a5397a90c88f","3434ef7b794bc505","b53660619b928eca"]]},{"id":"fcc95a7b1ccc30fa","type":"junction","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","x":540,"y":500,"wires":[["942640410dc2f972","a10cf95229654d4b","54df8bc58f816a0d"]]},{"id":"70cf7269c18b02c7","type":"comment","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Add further planes here","info":"One plane? Disable P2 node\n\nMore than two? Copy and add a Pn node\nto the list. Use a 10 second delay to\nallow time for the API call to return.","x":400,"y":260,"wires":[]},{"id":"af6e2c8fa93af511","type":"debug","z":"088f35b28bce9017","g":"a1385ae3163e5fb7","name":"Debug API & Dates","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":970,"y":120,"wires":[]},{"id":"46273418991a9fa0","type":"api-current-state","z":"088f35b28bce9017","d":true,"g":"a1385ae3163e5fb7","name":"Time","server":"","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"sensor.date_time_iso","state_type":"str","blockInputOverrides":true,"outputProperties":[{"property":"actual.hour","propertyType":"msg","value":"$substringAfter($entity().state,\"T\")~>$substringBefore(\":\")","valueType":"jsonata"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":530,"y":360,"wires":[["964720b6b93ae8d8"]]},{"id":"76e876790e22c53e","type":"ui_group","d":true,"name":"Solar Forecast","tab":"a7de90402f2427f9","order":7,"disp":true,"width":"15","collapse":false,"className":""},{"id":"025c69fc2ba0425c","type":"ha-entity-config","d":true,"server":"762fa2f33bfd32fc","deviceConfig":"","name":"SC Solar FC Power East","version":"6","entityType":"sensor","haConfig":[{"property":"name","value":"Solar FCP East"},{"property":"icon","value":""},{"property":"entity_category","value":""},{"property":"device_class","value":"power"},{"property":"unit_of_measurement","value":"W"},{"property":"state_class","value":"measurement"}],"resend":true,"debugEnabled":false},{"id":"4dba971878b0daa9","type":"ha-entity-config","d":true,"server":"762fa2f33bfd32fc","deviceConfig":"","name":"SC Solar FC Power West","version":"6","entityType":"sensor","haConfig":[{"property":"name","value":"Solar FCP West"},{"property":"icon","value":""},{"property":"entity_category","value":""},{"property":"device_class","value":"power"},{"property":"unit_of_measurement","value":"W"},{"property":"state_class","value":"measurement"}],"resend":true,"debugEnabled":false},{"id":"62602840fda5176b","type":"ha-entity-config","d":true,"server":"762fa2f33bfd32fc","deviceConfig":"","name":"SC FC Solar Table","version":"6","entityType":"sensor","haConfig":[{"property":"name","value":"FC table"},{"property":"icon","value":"mdi:update"},{"property":"entity_category","value":""},{"property":"device_class","value":""},{"property":"unit_of_measurement","value":""},{"property":"state_class","value":""}],"resend":true,"debugEnabled":false},{"id":"7fcde925c164fddb","type":"ha-entity-config","d":true,"server":"762fa2f33bfd32fc","deviceConfig":"","name":"SC FC Today","version":"6","entityType":"sensor","haConfig":[{"property":"name","value":"FC Estimate Today"},{"property":"icon","value":"mdi:counter"},{"property":"entity_category","value":""},{"property":"device_class","value":""},{"property":"unit_of_measurement","value":"kWh"},{"property":"state_class","value":""}],"resend":true,"debugEnabled":false},{"id":"eeac2705f3d9286b","type":"ha-entity-config","d":true,"server":"762fa2f33bfd32fc","deviceConfig":"","name":"SC for FC Tomorrow","version":"6","entityType":"sensor","haConfig":[{"property":"name","value":"FC Estimate Tomorrow"},{"property":"icon","value":"mdi:counter"},{"property":"entity_category","value":""},{"property":"device_class","value":""},{"property":"unit_of_measurement","value":"kWh"},{"property":"state_class","value":""}],"resend":true,"debugEnabled":false},{"id":"a7de90402f2427f9","type":"ui_tab","name":"Solar Forecast","icon":"dashboard","order":2,"disabled":false,"hidden":false}] |