98_feels_like.pm 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380
  1. # $Id: 98_feels_like.pm 16988 2018-07-16 07:23:38Z hotbso $
  2. ##############################################################################################################
  3. #
  4. # 98_feels_like.pm
  5. #
  6. # Module that listens to events generated by weather sensors in order to compute
  7. # - radiation
  8. # - clouds coverage
  9. # - equivalent temperature according to UTCI and Apparent Temperature (AT)
  10. #
  11. # using the solar radiation sensor of Fine Offset / Ambient Weather / WH2601 ... weather stations.
  12. #
  13. # See documentation at end.
  14. #
  15. # written by holger.a.teutsch@gmail.com
  16. #
  17. use strict;
  18. use warnings;
  19. # so "perl -w 98_feels_like.pm" should produce no syntax errors
  20. use vars qw($readingFnAttributes %defs);
  21. sub feels_like_Initialize($$)
  22. {
  23. my ($hash) = @_;
  24. $hash->{DefFn} = "feels_like_Define";
  25. $hash->{NotifyFn} = "feels_like_Notify";
  26. $hash->{AttrFn} = "feels_like_Attr";
  27. $hash->{NotifyOrderPrefix} = "48-"; # Want to be called earlier than default
  28. $hash->{AttrList} = "disable:0,1 maxTimediff sensorSkyViewFactor skyViewFactor " .
  29. "bowenRatio linkeTurbity directRadiationRatio coverageCallback utciWindCutoff " .
  30. "sunVisibility alphaMin alphaMax sensorType:wh2601,generic $readingFnAttributes";
  31. }
  32. ##########################
  33. sub
  34. feels_like_Define($$)
  35. {
  36. my ($hash, $def) = @_;
  37. my @a = split(/\s+/, $def);
  38. return "wrong syntax: " .
  39. "define <name> feels_like devicename temperature humidity " .
  40. "[wind_speed [solar_radiation [pressure]]]" if (@a < 4 || @a> 8);
  41. my $name = $a[0];
  42. my $devname = $a[2];
  43. my $match = 0;
  44. my $i = 3;
  45. foreach my $k ('T', 'H', 'W', 'S', 'P') {
  46. my $rk = $k . '_READING';
  47. my $dk = $k . '_DEV';
  48. # in case it's a redefine
  49. delete($hash->{$dk});
  50. delete($hash->{$rk});
  51. if($#a >= $i) {
  52. my @x = split(/:/, $a[$i]);
  53. if (@x == 2) {
  54. $hash->{$dk} = $x[0];
  55. $hash->{$rk} = $x[1];
  56. } else {
  57. $hash->{$dk} = $devname;
  58. $hash->{$rk} = $x[0];
  59. $match++;
  60. }
  61. }
  62. $i++;
  63. }
  64. return "feels_like: at least one reading device must match $devname" if ($match == 0);
  65. $hash->{DEVNAME} = $devname;
  66. # set NOTIFYDEV
  67. $hash->{NOTIFYDEV} = $devname;
  68. $hash->{STATE} = "active";
  69. return undef;
  70. }
  71. ##########################
  72. sub
  73. feels_like_Attr(@)
  74. {
  75. my ($cmd, $name, $a_name, $a_val) = @_;
  76. my $hash = $defs{$name};
  77. if ($cmd eq "set" && $a_name eq "sunVisibility") {
  78. my @x = split(/, */, $a_val);
  79. if ((@x & 1) == 0 || $x[0] != 0 || $x[$#x] != 360) {
  80. return "Value of sunVisibility must must be a list odd # of elements "
  81. . "starting with 0 and ending with 360";
  82. }
  83. }
  84. if ($a_name eq 'disable') {
  85. if ($cmd eq 'set') {
  86. $hash->{DISABLED} = $a_val;
  87. $hash->{STATE} = $a_val == 1 ? 'disabled' : 'active';
  88. }
  89. elsif ($cmd eq 'del') {
  90. $hash->{DISABLED} = 0;
  91. $hash->{STATE} = 'active';
  92. }
  93. }
  94. return undef;
  95. }
  96. ##########################
  97. sub
  98. feels_like_Notify($$)
  99. {
  100. my ($hash, $dev) = @_;
  101. my $hash_name = $hash->{NAME};
  102. my $dev_name = $dev->{NAME};
  103. return undef if ($hash->{DISABLED});
  104. Log3($hash_name, 4, "feels_like_Notify: $hash_name for $dev_name");
  105. my $max_timediff = AttrVal($hash_name, "maxTimediff", 10 * 60);
  106. # extract readings from event queue
  107. my @R = ('', '', '', '', '');
  108. my $events = deviceEvents($dev, undef);
  109. return undef if (!$events);
  110. my $have_one = 0;
  111. foreach my $ev (@{$events}) {
  112. next if (!defined($ev));
  113. my ($ev_name, $val, $rest) = split(/: +/, $ev, 3);
  114. next if (!defined($ev_name) || !defined($val));
  115. Log3($hash_name, 5, "feels_like_Notify ev_name='$ev_name' val='$val'");
  116. my $i = 0;
  117. foreach my $k ('T', 'H', 'W', 'S', 'P') {
  118. $i++;
  119. my $dk = $k . '_DEV';
  120. next if (!exists($hash->{$dk}) || ($hash->{$dk} ne $dev_name));
  121. my $rk = $k . '_READING';
  122. next if (!exists($hash->{$rk}) || ($hash->{$rk} ne $ev_name));
  123. $R[$i-1] = $val;
  124. $have_one = 1;
  125. }
  126. }
  127. # at least one match required
  128. return undef unless $have_one;
  129. my ($T, $H, $W, $S, $P) = @R;
  130. Log3($hash_name, 4, "feels_like_Notify $dev_name events T: $T, H: $H, W: $W, S: $S, P: $P");
  131. # now get readings that were not filled by events
  132. my $i = 0;
  133. foreach my $k ('T', 'H', 'W', 'S', 'P') {
  134. $i++;
  135. my $rk = $k . '_READING';
  136. next if (!exists($hash->{$rk}) || $R[$i-1]);
  137. my $dk = $k . '_DEV';
  138. my $val = ReadingsNum($hash->{$dk}, $hash->{$rk}, undef);
  139. if (!defined($val)) {
  140. Log(1, "feels_like: reading $hash->{$dk}:$hash->{$rk} does not exist, sorry!");
  141. return undef;
  142. }
  143. my $ra = ReadingsAge($hash->{$dk}, $hash->{$rk}, undef);
  144. if (!defined($ra) || $ra > $max_timediff) {
  145. Log(1, "feels_like: reading $hash->{$dk}:$hash->{$rk} is too old, sorry!");
  146. return undef;
  147. }
  148. $R[$i-1] = $val;
  149. }
  150. ($T, $H, $W, $S, $P) = @R;
  151. Log3($hash_name, 4, "feels_like_Notify final T: $T, H: $H, W: $W, S: $S, P: $P");
  152. # running as device, save name for log level determination
  153. # in library calls
  154. $feels_like::dev_instance = $hash_name;
  155. readingsBeginUpdate($dev);
  156. my ($T_tmrt, $ER, $coverage, $oktas, $clouds, $alt, $azimuth);
  157. $W = 0 if ($W eq '');
  158. $P = 1013 if ($P eq '');
  159. if ($S ne '') {
  160. readingsBeginUpdate($hash);
  161. ($T_tmrt, $ER) = mean_radiant_temperature($hash_name, $T, $H, $W, $P, $S);
  162. readingsBulkUpdate($hash, "sun_visible", $hash->{helper}{sun_visible});
  163. readingsBulkUpdate($hash, "sr_invalid", $hash->{helper}{sr_invalid});
  164. my $x = $hash->{helper}{alpha};
  165. readingsBulkUpdate($hash, "alpha", defined($x) ? round($x, 2) : '');
  166. $x = $hash->{helper}{nr_stddev};
  167. readingsBulkUpdate($hash, "nr_stddev", defined($x) ? round($x, 3) : '');
  168. $x = $hash->{helper}{nr_current};
  169. readingsBulkUpdate($hash, "nr_current", defined($x) ? round($x, 3) : '');
  170. my ($oktas, $clouds);
  171. my $coverage = $hash->{helper}{nr_coverage};
  172. if (defined($coverage)) {
  173. $coverage = round($coverage, 2);
  174. $oktas = round($coverage * 8, 0);
  175. $clouds = ('SKC', 'FEW', 'FEW', 'SCT', 'SCT', 'BKN', 'BKN', 'BKN', 'OVC')[$oktas];
  176. } else {
  177. $coverage = $oktas = $clouds = '';
  178. }
  179. readingsBulkUpdate($hash, "nr_coverage", $coverage);
  180. readingsBulkUpdate($hash, "nr_oktas", $oktas);
  181. readingsBulkUpdate($hash, "nr_clouds", $clouds);
  182. $coverage = $hash->{helper}{coverage};
  183. if (defined($coverage)) {
  184. $coverage = round($coverage, 2);
  185. $oktas = round($coverage * 8, 0);
  186. $clouds = ('SKC', 'FEW', 'FEW', 'SCT', 'SCT', 'BKN', 'BKN', 'BKN', 'OVC')[$oktas];
  187. } else {
  188. $coverage = $oktas = $clouds = '';
  189. }
  190. readingsBulkUpdate($dev, "extra_radiation", round($ER, 1));
  191. readingsBulkUpdate($dev, "coverage", $coverage);
  192. readingsBulkUpdate($dev, "oktas", $oktas);
  193. readingsBulkUpdate($dev, "clouds", $clouds);
  194. readingsEndUpdate($hash, 1);
  195. } else {
  196. $T_tmrt = $T;
  197. $ER = 0;
  198. }
  199. # for higher winds UTCI seems a bit odd
  200. my $max_W = AttrVal($hash_name, "utciWindCutoff", 3.0);
  201. my $T_utci = T_UTCI($T, $H, min($W, $max_W), $P, $T_tmrt);
  202. my $AT = T_apparent_temperature($T, $H, $W, $ER);
  203. readingsBulkUpdate($dev, "temperature_utci", round($T_utci, 1));
  204. readingsBulkUpdate($dev, "temperature_mrt", round($T_tmrt, 1));
  205. readingsBulkUpdate($dev, "temperature_at", round($AT, 1));
  206. readingsEndUpdate($dev, 1);
  207. $feels_like::dev_instance = undef;
  208. return undef;
  209. }
  210. # don't pollute main namespace
  211. package feels_like;
  212. use POSIX;
  213. use Math::Trig qw (deg2rad rad2deg);
  214. # this stuff is needed so the core functions can be called (and debugged) outside of FHEM
  215. # without too much pain
  216. our $verbose = 1;
  217. our $dev_instance = undef;
  218. sub Log($$)
  219. {
  220. my ($level, $str) = @_;
  221. if (defined($dev_instance)) {
  222. main::Log3($dev_instance, $level, "feels_like_core " . $str);
  223. } else {
  224. return unless($level <= $verbose);
  225. main::Log(1, "feels_like_lib " . $str);
  226. }
  227. }
  228. # Derived by Holger Teutsch from -- UTCI_a002.f90 -- http://www.utci.org
  229. #~ value is the UTCI in degree Celsius
  230. #~ computed by a 6th order approximating polynomial from the 4 Input paramters
  231. #~
  232. #~ Input parameters
  233. #~ - Ta : air temperature, degree Celsius
  234. #~ - ehPa : water $vapour presure, hPa=hecto Pascal
  235. #~ - Tmrt : mean radiant temperature, degree Celsius
  236. #~ - va : wind speed 10 m above ground level in m/s
  237. #~
  238. #~ UTCI_approx, Version a 0.002, October 2009
  239. #~ Copyright (C) 2009 Peter Broede
  240. sub UTCI_approx($$$$)
  241. {
  242. my ($Ta, $ehPa, $Tmrt, $va) = @_;
  243. my $D_Tmrt = $Tmrt - $Ta;
  244. # approx is only valid for va >= 0.5 m/s
  245. $va = 0.5 if ($va < 0.5);
  246. my $Pa = $ehPa/10.0; #~ use $vapour pressure in kPa
  247. #~ calculate 6th order polynomial as approximation
  248. my $Ta_2 = $Ta*$Ta;
  249. my $Ta_3 = $Ta_2*$Ta;
  250. my $Ta_4 = $Ta_3*$Ta;
  251. my $Ta_5 = $Ta_4*$Ta;
  252. my $va_2 = $va*$va;
  253. my $va_3 = $va_2*$va;
  254. my $va_4 = $va_3*$va;
  255. my $va_5 = $va_4*$va;
  256. my $D_Tmrt_2 = $D_Tmrt*$D_Tmrt;
  257. my $D_Tmrt_3 = $D_Tmrt_2*$D_Tmrt;
  258. my $D_Tmrt_4 = $D_Tmrt_3*$D_Tmrt;
  259. my $D_Tmrt_5 = $D_Tmrt_4*$D_Tmrt;
  260. my $Pa_2 = $Pa*$Pa;
  261. my $Pa_3 = $Pa_2*$Pa;
  262. my $Pa_4 = $Pa_3*$Pa;
  263. my $Pa_5 = $Pa_4*$Pa;
  264. my $UTCI_approx = $Ta +
  265. 6.07562052E-01 +
  266. -2.27712343E-02 * $Ta +
  267. 8.06470249E-04 * $Ta_2 +
  268. -1.54271372E-04 * $Ta_3 +
  269. -3.24651735E-06 * $Ta_4 +
  270. 7.32602852E-08 * $Ta_5 +
  271. 1.35959073E-09 * $Ta_5*$Ta +
  272. -2.25836520E+00 * $va +
  273. 8.80326035E-02 * $Ta*$va +
  274. 2.16844454E-03 * $Ta_2*$va +
  275. -1.53347087E-05 * $Ta_3*$va +
  276. -5.72983704E-07 * $Ta_4*$va +
  277. -2.55090145E-09 * $Ta_5*$va +
  278. -7.51269505E-01 * $va_2 +
  279. -4.08350271E-03 * $Ta*$va_2 +
  280. -5.21670675E-05 * $Ta_2*$va_2 +
  281. 1.94544667E-06 * $Ta_3*$va_2 +
  282. 1.14099531E-08 * $Ta_4*$va_2 +
  283. 1.58137256E-01 * $va_3 +
  284. -6.57263143E-05 * $Ta*$va_3 +
  285. 2.22697524E-07 * $Ta_2*$va_3 +
  286. -4.16117031E-08 * $Ta_3*$va_3 +
  287. -1.27762753E-02 * $va_4 +
  288. 9.66891875E-06 * $Ta*$va_4 +
  289. 2.52785852E-09 * $Ta_2*$va_4 +
  290. 4.56306672E-04 * $va_5 +
  291. -1.74202546E-07 * $Ta*$va_5 +
  292. -5.91491269E-06 * $va_5*$va +
  293. 3.98374029E-01 * $D_Tmrt +
  294. 1.83945314E-04 * $Ta*$D_Tmrt +
  295. -1.73754510E-04 * $Ta_2*$D_Tmrt +
  296. -7.60781159E-07 * $Ta_3*$D_Tmrt +
  297. 3.77830287E-08 * $Ta_4*$D_Tmrt +
  298. 5.43079673E-10 * $Ta_5*$D_Tmrt +
  299. -2.00518269E-02 * $va*$D_Tmrt +
  300. 8.92859837E-04 * $Ta*$va*$D_Tmrt +
  301. 3.45433048E-06 * $Ta_2*$va*$D_Tmrt +
  302. -3.77925774E-07 * $Ta_3*$va*$D_Tmrt +
  303. -1.69699377E-09 * $Ta_4*$va*$D_Tmrt +
  304. 1.69992415E-04 * $va_2*$D_Tmrt +
  305. -4.99204314E-05 * $Ta*$va_2*$D_Tmrt +
  306. 2.47417178E-07 * $Ta_2*$va_2*$D_Tmrt +
  307. 1.07596466E-08 * $Ta_3*$va_2*$D_Tmrt +
  308. 8.49242932E-05 * $va_3*$D_Tmrt +
  309. 1.35191328E-06 * $Ta*$va_3*$D_Tmrt +
  310. -6.21531254E-09 * $Ta_2*$va_3*$D_Tmrt +
  311. -4.99410301E-06 * $va_4*$D_Tmrt +
  312. -1.89489258E-08 * $Ta*$va_4*$D_Tmrt +
  313. 8.15300114E-08 * $va_5*$D_Tmrt +
  314. 7.55043090E-04 * $D_Tmrt_2 +
  315. -5.65095215E-05 * $Ta*$D_Tmrt_2 +
  316. -4.52166564E-07 * $Ta_2*$D_Tmrt_2 +
  317. 2.46688878E-08 * $Ta_3*$D_Tmrt_2 +
  318. 2.42674348E-10 * $Ta_4*$D_Tmrt_2 +
  319. 1.54547250E-04 * $va*$D_Tmrt_2 +
  320. 5.24110970E-06 * $Ta*$va*$D_Tmrt_2 +
  321. -8.75874982E-08 * $Ta_2*$va*$D_Tmrt_2 +
  322. -1.50743064E-09 * $Ta_3*$va*$D_Tmrt_2 +
  323. -1.56236307E-05 * $va_2*$D_Tmrt_2 +
  324. -1.33895614E-07 * $Ta*$va_2*$D_Tmrt_2 +
  325. 2.49709824E-09 * $Ta_2*$va_2*$D_Tmrt_2 +
  326. 6.51711721E-07 * $va_3*$D_Tmrt_2 +
  327. 1.94960053E-09 * $Ta*$va_3*$D_Tmrt_2 +
  328. -1.00361113E-08 * $va_4*$D_Tmrt_2 +
  329. -1.21206673E-05 * $D_Tmrt_3 +
  330. -2.18203660E-07 * $Ta*$D_Tmrt_3 +
  331. 7.51269482E-09 * $Ta_2*$D_Tmrt_3 +
  332. 9.79063848E-11 * $Ta_3*$D_Tmrt_3 +
  333. 1.25006734E-06 * $va*$D_Tmrt_3 +
  334. -1.81584736E-09 * $Ta*$va*$D_Tmrt_3 +
  335. -3.52197671E-10 * $Ta_2*$va*$D_Tmrt_3 +
  336. -3.36514630E-08 * $va_2*$D_Tmrt_3 +
  337. 1.35908359E-10 * $Ta*$va_2*$D_Tmrt_3 +
  338. 4.17032620E-10 * $va_3*$D_Tmrt_3 +
  339. -1.30369025E-09 * $D_Tmrt_4 +
  340. 4.13908461E-10 * $Ta*$D_Tmrt_4 +
  341. 9.22652254E-12 * $Ta_2*$D_Tmrt_4 +
  342. -5.08220384E-09 * $va*$D_Tmrt_4 +
  343. -2.24730961E-11 * $Ta*$va*$D_Tmrt_4 +
  344. 1.17139133E-10 * $va_2*$D_Tmrt_4 +
  345. 6.62154879E-10 * $D_Tmrt_5 +
  346. 4.03863260E-13 * $Ta*$D_Tmrt_5 +
  347. 1.95087203E-12 * $va*$D_Tmrt_5 +
  348. -4.73602469E-12 * $D_Tmrt_5*$D_Tmrt +
  349. 5.12733497E+00 * $Pa +
  350. -3.12788561E-01 * $Ta*$Pa +
  351. -1.96701861E-02 * $Ta_2*$Pa +
  352. 9.99690870E-04 * $Ta_3*$Pa +
  353. 9.51738512E-06 * $Ta_4*$Pa +
  354. -4.66426341E-07 * $Ta_5*$Pa +
  355. 5.48050612E-01 * $va*$Pa +
  356. -3.30552823E-03 * $Ta*$va*$Pa +
  357. -1.64119440E-03 * $Ta_2*$va*$Pa +
  358. -5.16670694E-06 * $Ta_3*$va*$Pa +
  359. 9.52692432E-07 * $Ta_4*$va*$Pa +
  360. -4.29223622E-02 * $va_2*$Pa +
  361. 5.00845667E-03 * $Ta*$va_2*$Pa +
  362. 1.00601257E-06 * $Ta_2*$va_2*$Pa +
  363. -1.81748644E-06 * $Ta_3*$va_2*$Pa +
  364. -1.25813502E-03 * $va_3*$Pa +
  365. -1.79330391E-04 * $Ta*$va_3*$Pa +
  366. 2.34994441E-06 * $Ta_2*$va_3*$Pa +
  367. 1.29735808E-04 * $va_4*$Pa +
  368. 1.29064870E-06 * $Ta*$va_4*$Pa +
  369. -2.28558686E-06 * $va_5*$Pa +
  370. -3.69476348E-02 * $D_Tmrt*$Pa +
  371. 1.62325322E-03 * $Ta*$D_Tmrt*$Pa +
  372. -3.14279680E-05 * $Ta_2*$D_Tmrt*$Pa +
  373. 2.59835559E-06 * $Ta_3*$D_Tmrt*$Pa +
  374. -4.77136523E-08 * $Ta_4*$D_Tmrt*$Pa +
  375. 8.64203390E-03 * $va*$D_Tmrt*$Pa +
  376. -6.87405181E-04 * $Ta*$va*$D_Tmrt*$Pa +
  377. -9.13863872E-06 * $Ta_2*$va*$D_Tmrt*$Pa +
  378. 5.15916806E-07 * $Ta_3*$va*$D_Tmrt*$Pa +
  379. -3.59217476E-05 * $va_2*$D_Tmrt*$Pa +
  380. 3.28696511E-05 * $Ta*$va_2*$D_Tmrt*$Pa +
  381. -7.10542454E-07 * $Ta_2*$va_2*$D_Tmrt*$Pa +
  382. -1.24382300E-05 * $va_3*$D_Tmrt*$Pa +
  383. -7.38584400E-09 * $Ta*$va_3*$D_Tmrt*$Pa +
  384. 2.20609296E-07 * $va_4*$D_Tmrt*$Pa +
  385. -7.32469180E-04 * $D_Tmrt_2*$Pa +
  386. -1.87381964E-05 * $Ta*$D_Tmrt_2*$Pa +
  387. 4.80925239E-06 * $Ta_2*$D_Tmrt_2*$Pa +
  388. -8.75492040E-08 * $Ta_3*$D_Tmrt_2*$Pa +
  389. 2.77862930E-05 * $va*$D_Tmrt_2*$Pa +
  390. -5.06004592E-06 * $Ta*$va*$D_Tmrt_2*$Pa +
  391. 1.14325367E-07 * $Ta_2*$va*$D_Tmrt_2*$Pa +
  392. 2.53016723E-06 * $va_2*$D_Tmrt_2*$Pa +
  393. -1.72857035E-08 * $Ta*$va_2*$D_Tmrt_2*$Pa +
  394. -3.95079398E-08 * $va_3*$D_Tmrt_2*$Pa +
  395. -3.59413173E-07 * $D_Tmrt_3*$Pa +
  396. 7.04388046E-07 * $Ta*$D_Tmrt_3*$Pa +
  397. -1.89309167E-08 * $Ta_2*$D_Tmrt_3*$Pa +
  398. -4.79768731E-07 * $va*$D_Tmrt_3*$Pa +
  399. 7.96079978E-09 * $Ta*$va*$D_Tmrt_3*$Pa +
  400. 1.62897058E-09 * $va_2*$D_Tmrt_3*$Pa +
  401. 3.94367674E-08 * $D_Tmrt_4*$Pa +
  402. -1.18566247E-09 * $Ta*$D_Tmrt_4*$Pa +
  403. 3.34678041E-10 * $va*$D_Tmrt_4*$Pa +
  404. -1.15606447E-10 * $D_Tmrt_5*$Pa +
  405. -2.80626406E+00 * $Pa_2 +
  406. 5.48712484E-01 * $Ta*$Pa_2 +
  407. -3.99428410E-03 * $Ta_2*$Pa_2 +
  408. -9.54009191E-04 * $Ta_3*$Pa_2 +
  409. 1.93090978E-05 * $Ta_4*$Pa_2 +
  410. -3.08806365E-01 * $va*$Pa_2 +
  411. 1.16952364E-02 * $Ta*$va*$Pa_2 +
  412. 4.95271903E-04 * $Ta_2*$va*$Pa_2 +
  413. -1.90710882E-05 * $Ta_3*$va*$Pa_2 +
  414. 2.10787756E-03 * $va_2*$Pa_2 +
  415. -6.98445738E-04 * $Ta*$va_2*$Pa_2 +
  416. 2.30109073E-05 * $Ta_2*$va_2*$Pa_2 +
  417. 4.17856590E-04 * $va_3*$Pa_2 +
  418. -1.27043871E-05 * $Ta*$va_3*$Pa_2 +
  419. -3.04620472E-06 * $va_4*$Pa_2 +
  420. 5.14507424E-02 * $D_Tmrt*$Pa_2 +
  421. -4.32510997E-03 * $Ta*$D_Tmrt*$Pa_2 +
  422. 8.99281156E-05 * $Ta_2*$D_Tmrt*$Pa_2 +
  423. -7.14663943E-07 * $Ta_3*$D_Tmrt*$Pa_2 +
  424. -2.66016305E-04 * $va*$D_Tmrt*$Pa_2 +
  425. 2.63789586E-04 * $Ta*$va*$D_Tmrt*$Pa_2 +
  426. -7.01199003E-06 * $Ta_2*$va*$D_Tmrt*$Pa_2 +
  427. -1.06823306E-04 * $va_2*$D_Tmrt*$Pa_2 +
  428. 3.61341136E-06 * $Ta*$va_2*$D_Tmrt*$Pa_2 +
  429. 2.29748967E-07 * $va_3*$D_Tmrt*$Pa_2 +
  430. 3.04788893E-04 * $D_Tmrt_2*$Pa_2 +
  431. -6.42070836E-05 * $Ta*$D_Tmrt_2*$Pa_2 +
  432. 1.16257971E-06 * $Ta_2*$D_Tmrt_2*$Pa_2 +
  433. 7.68023384E-06 * $va*$D_Tmrt_2*$Pa_2 +
  434. -5.47446896E-07 * $Ta*$va*$D_Tmrt_2*$Pa_2 +
  435. -3.59937910E-08 * $va_2*$D_Tmrt_2*$Pa_2 +
  436. -4.36497725E-06 * $D_Tmrt_3*$Pa_2 +
  437. 1.68737969E-07 * $Ta*$D_Tmrt_3*$Pa_2 +
  438. 2.67489271E-08 * $va*$D_Tmrt_3*$Pa_2 +
  439. 3.23926897E-09 * $D_Tmrt_4*$Pa_2 +
  440. -3.53874123E-02 * $Pa_3 +
  441. -2.21201190E-01 * $Ta*$Pa_3 +
  442. 1.55126038E-02 * $Ta_2*$Pa_3 +
  443. -2.63917279E-04 * $Ta_3*$Pa_3 +
  444. 4.53433455E-02 * $va*$Pa_3 +
  445. -4.32943862E-03 * $Ta*$va*$Pa_3 +
  446. 1.45389826E-04 * $Ta_2*$va*$Pa_3 +
  447. 2.17508610E-04 * $va_2*$Pa_3 +
  448. -6.66724702E-05 * $Ta*$va_2*$Pa_3 +
  449. 3.33217140E-05 * $va_3*$Pa_3 +
  450. -2.26921615E-03 * $D_Tmrt*$Pa_3 +
  451. 3.80261982E-04 * $Ta*$D_Tmrt*$Pa_3 +
  452. -5.45314314E-09 * $Ta_2*$D_Tmrt*$Pa_3 +
  453. -7.96355448E-04 * $va*$D_Tmrt*$Pa_3 +
  454. 2.53458034E-05 * $Ta*$va*$D_Tmrt*$Pa_3 +
  455. -6.31223658E-06 * $va_2*$D_Tmrt*$Pa_3 +
  456. 3.02122035E-04 * $D_Tmrt_2*$Pa_3 +
  457. -4.77403547E-06 * $Ta*$D_Tmrt_2*$Pa_3 +
  458. 1.73825715E-06 * $va*$D_Tmrt_2*$Pa_3 +
  459. -4.09087898E-07 * $D_Tmrt_3*$Pa_3 +
  460. 6.14155345E-01 * $Pa_4 +
  461. -6.16755931E-02 * $Ta*$Pa_4 +
  462. 1.33374846E-03 * $Ta_2*$Pa_4 +
  463. 3.55375387E-03 * $va*$Pa_4 +
  464. -5.13027851E-04 * $Ta*$va*$Pa_4 +
  465. 1.02449757E-04 * $va_2*$Pa_4 +
  466. -1.48526421E-03 * $D_Tmrt*$Pa_4 +
  467. -4.11469183E-05 * $Ta*$D_Tmrt*$Pa_4 +
  468. -6.80434415E-06 * $va*$D_Tmrt*$Pa_4 +
  469. -9.77675906E-06 * $D_Tmrt_2*$Pa_4 +
  470. 8.82773108E-02 * $Pa_5 +
  471. -3.01859306E-03 * $Ta*$Pa_5 +
  472. 1.04452989E-03 * $va*$Pa_5 +
  473. 2.47090539E-04 * $D_Tmrt*$Pa_5 +
  474. 1.48348065E-03 * $Pa_5*$Pa;
  475. return $UTCI_approx;
  476. }
  477. #~ calculates saturation vapour pressure over water in hPa for input air temperature (ta) in celsius according to:
  478. #~ Hardy, R.; ITS-90 Formulations for Vapor Pressure, Frostpoint Temperature,
  479. # Dewpoint Temperature and Enhancement Factors in the Range -100 to 100 °C;
  480. #~ Proceedings of Third International Symposium on Humidity and Moisture;
  481. # edited by National Physical Laboratory (NPL), London, 1998, pp. 214-221
  482. #~ http://www.thunderscientific.com/tech_info/reflibrary/its90formulas.pdf (retrieved 2008-10-01)
  483. sub es($)
  484. {
  485. my ($ta) = @_;
  486. my @g = (-2.8365744E3, -6.028076559E3, 1.954263612E1, -2.737830188E-2, 1.6261698E-5, 7.0229056E-10,
  487. -1.8680009E-13, 2.7150305);
  488. my $tk = $ta + 273.15; # air temp in K
  489. my $res = $g[7] * log($tk);
  490. for (my $i = 0; $i < 7; $i++) {
  491. $res += $g[$i] *$tk**($i-2);
  492. }
  493. $res=exp($res)*0.01; # *0.01: convert Pa to hPa
  494. return $res;
  495. }
  496. # http://www.bom.gov.au/info/thermal_stress/#atapproximation
  497. #
  498. # [STDM 1994] : Steadman: Norms of apparent temperature in Australia
  499. # http://reg.bom.gov.au/amm/docs/1994/steadman.pdf
  500. #
  501. sub apparent_temperature($$$$)
  502. {
  503. # ambient Temperature (C), humidity (%), windspeed in 10 m (m/s)
  504. # effective radiation (W/m^2)
  505. my ($Ta, $rh, $ws, $ER) = @_;
  506. # vapour pressure
  507. my $e = $rh / 100.0 * es($Ta);
  508. # apparent temperature
  509. my $AT = $Ta + 0.348 * $e -0.7 * $ws + 0.7 * $ER / ($ws + 10) - 4.25;
  510. return $AT;
  511. }
  512. sub cosD($)
  513. {
  514. my ($phi) = @_;
  515. return cos($phi * 0.0174532925);
  516. }
  517. sub sinD($)
  518. {
  519. my ($phi) = @_;
  520. return sin($phi * 0.0174532925);
  521. }
  522. sub asinD($)
  523. {
  524. my ($x) = @_;
  525. return POSIX::asin($x) / 0.0174532925;
  526. }
  527. sub acosD($)
  528. {
  529. my ($x) = @_;
  530. return POSIX::acos($x) / 0.0174532925;
  531. }
  532. # tweakable constants
  533. our @T_L = (3.4, 3.6, 4.3, 4.2, 4.7, 4.6, 4.7, 4.7, 4.3, 3.8, 3.4, 3.4); # Linke turbity for each month
  534. our $f_svf_fl = 0.7; # sky view factor for computation of T_mrt
  535. our $f_svf_sensor = 0.6; # sky view factor of sensor
  536. our $bowen = 0.6; # bowen ratio (default = suburb)
  537. # given constants
  538. my $I_0 = 1350; # global insolation W/m²
  539. my $sigma = 5.6E-8; # Stefan-Boltzmann
  540. my $sw_abs = 0.72; # short wave absoption of body
  541. my $lw_abs = 0.95; # long wave
  542. ##################################
  543. # In the morning hours and or low altitudes of the sun the radiation sensor is obstructed by the rain gauge.
  544. # Therefore the radiation curve is heavily skewed to the afternoon.
  545. # Using several samplings af clear day radiation over the year a correction factor for the sensor was developed.
  546. # The assumption is that the sensor always measures the diffuse radiation and that the direct or normal
  547. # part is to be corrected.
  548. #
  549. # A polynom regression created with Python's sciphi.curve_fit is used here.
  550. #
  551. # I_h = poly(azimuth, sday) * (I_measured - D)
  552. # sday is the distance to June 21st in days
  553. #
  554. my $azimuth_min = 80;
  555. my $azimuth_max = 280;
  556. my $alpha_min = 0.28;
  557. my $alpha_max = 3.20;
  558. my $nx = 7;
  559. my $ny = 11;
  560. my @poly = (-106.92078815088152,1.1559639672742617,-0.10641701193735834,
  561. -0.003289004759141109,0.00024341610319517833,-7.393885292177471e-06,
  562. 1.1036469480601365e-07,-8.433653719722458e-10,4.6816105400180725e-12,
  563. -2.2915699724588908e-14,5.1558807208283076e-17,-1.079309274936794e-21,
  564. 4.475568377940087,-0.02979654850540337,0.004700175671662923,
  565. -2.0556721233089508e-05,-6.038914362044792e-08,2.006751148263329e-08,
  566. -3.111327815305771e-10,-1.374995798484272e-12,1.9735294352791308e-14,
  567. 2.1301007272254039e-19,-1.1889565579254942e-22,-6.300423390960505e-22,
  568. -0.07727338274859613,0.00020814642590810098,-6.394225596258378e-05,
  569. 7.868630252758761e-09,-7.464104467557937e-09,1.1747261979615144e-10,
  570. 6.942150907013742e-13,3.4379341050323286e-15,6.450994446627346e-18,
  571. -4.444113964025086e-19,6.797271157288228e-22,-2.4187064817431376e-26,
  572. 0.0007233278231093484,-2.738439691372625e-07,5.245059294961144e-07,
  573. 3.0960830052172135e-09,-8.666235563108327e-12,-8.543981121106956e-13,
  574. -3.0921133374358243e-16,-3.403768199365476e-18,2.0279200191426042e-21,
  575. -4.248840692118507e-21,1.7318873922658786e-23,8.574899408718153e-28,
  576. -3.97231902915211e-06,-5.352290412779365e-10,-3.192650063883535e-09,
  577. -8.74015720283199e-12,2.283911280963708e-13,7.341513792108321e-16,
  578. -1.646709199019733e-19,1.3337349967893863e-19,-1.768592696157379e-24,
  579. 1.8081165441415707e-23,-9.255858370952993e-28,-6.417530710544851e-29,
  580. 1.2816665080935244e-08,-1.1922083845200477e-11,1.3421147775930614e-11,
  581. -8.568582018392473e-14,1.9981576527458447e-16,-3.2157555342893155e-22,
  582. 6.697464022398249e-20,-1.7492784034332204e-21,2.9982339842976173e-26,
  583. 3.1608055013268483e-26,-4.358123093981532e-28,-4.283467678330636e-31,
  584. -2.2523634293967502e-11,7.160123195453301e-14,-3.16054616229181e-14,
  585. 4.4391029509107566e-16,-3.1617351024523765e-18,-1.106231164369411e-20,
  586. 1.3620891422547596e-26,4.542693150881559e-24,-1.1189308112788874e-26,
  587. -2.7544251659486844e-29,1.8059972620664227e-36,6.576920263313157e-33,
  588. 1.6644569253648212e-14,-1.0182652809916555e-16,2.986157294663078e-17,
  589. -5.245136086661749e-19,1.8660756935566013e-21,1.0544047345884695e-22,
  590. -1.6410689680364017e-24,1.0506963847508829e-26,-5.70549250231417e-29,
  591. 3.5859454340809775e-32,2.027506749740463e-33,-1.4181289166467323e-35);
  592. # numdp: 3936, mean error: 0.06565
  593. # azimuth, sday
  594. sub sensor_factor($$) {
  595. my ($x, $y) = @_;
  596. # precompute powers
  597. my @yp;
  598. my $t = 1.0;
  599. for my $i (0 .. $ny) {
  600. push(@yp, $t);
  601. $t *= $y;
  602. }
  603. my $f = 0.0;
  604. my $xp = 1.0;
  605. for my $i (0 .. $nx) {
  606. for my $j (0 .. $ny) {
  607. $f += $poly[$i * ($ny + 1) +$j] * $xp * $yp[$j];
  608. }
  609. $xp *= $x;
  610. }
  611. return $f;
  612. }
  613. ##########################
  614. sub Julian_Date($$$$)
  615. {
  616. my ($yr, $mn, $mday, $hr) = @_;
  617. # http://aa.usno.navy.mil/faq/docs/JD_Formula.php
  618. my $JD = 367 * $yr - int(7 * ($yr + int(($mn+9)/12)) / 4) + int(275 * $mn/9) + $mday + 1721013.5 + $hr / 24;
  619. }
  620. # get (azimuth, elevation) of sun
  621. # https://de.wikipedia.org/wiki/Sonnenstand#Astronomische_Zusammenh%C3%A4nge
  622. sub sun_position($$$)
  623. {
  624. my ($LAT, $LON, $TS) = @_;
  625. my ($sec, $min, $hr, $mday, $mn, $yr) = gmtime($TS);
  626. $yr += 1900;
  627. $mn += 1;
  628. $hr += $min / 60;
  629. my $JD = Julian_Date($yr, $mn, $mday, $hr);
  630. my $n = $JD - 2451545.0;
  631. my $L = fmod(280.460 + 0.9856474 * $n, 360);
  632. my $g = fmod(357.528 + 0.9856003 * $n, 360);
  633. my $Lambda = fmod($L + 1.915 * sinD($g) + 0.01997 * sinD(2 * $g), 360);
  634. my $eps = 23.439 - 0.0000004 * $n;
  635. my $sL = sinD($Lambda);
  636. my $cL = cosD($Lambda);
  637. my $alpha = rad2deg(atan(cosD($eps) * $sL / $cL));
  638. if ($cL < 0) {
  639. $alpha += 180;
  640. }
  641. my $delta = asinD(sinD($eps) * sinD($Lambda));
  642. my $T0 = (Julian_Date($yr, $mn, $mday, 0) - 2451545.0) / 36525;
  643. my $Theta_hG = fmod(6.697376 + 2400.05134 * $T0 + 1.002738 * $hr, 24);
  644. my $Theta = fmod($Theta_hG * 15 + $LON, 360);
  645. my $tau = $Theta - $alpha;
  646. my $sd = sinD($delta);
  647. my $cd = cosD($delta);
  648. my $X = cosD($tau) * sinD($LAT) - $sd / $cd * cosD($LAT);
  649. my $a = rad2deg(atan(sinD($tau) / $X));
  650. $a += 180 if ($X < 0);
  651. $a = fmod($a + 180, 360);
  652. my $h = asinD($cd * cosD($tau) * cosD($LAT) + $sd * sinD($LAT));
  653. my $R = 1.02 / tan(deg2rad($h + 10.3 / ($h + 5.11)));
  654. my $hR = $h + $R / 60;
  655. Log(5, "JD: $JD, n: $n, L: $L, g: $g, Lambda: $Lambda, Eps: $eps, alpha: $alpha, delta: $delta\n");
  656. Log(5, "T0: $T0, Theta_hG: $Theta_hG, Theta: $Theta, a: $a, h: $h, h: $hR\n");
  657. return ($a, $h);
  658. }
  659. # compute radiation components for sun altitude, pressure, and sky view factor
  660. sub radiation($$$$)
  661. {
  662. my ($alt, $pabs, $f_svf, $f_direct) = @_;
  663. my ($month, $day_of_year) = (gmtime(time()))[4, 7];
  664. my $t_l = $T_L[$month];
  665. # Steadman 1994, eq. (18)
  666. my $I_0_d = ($I_0 - 46 * sinD(($day_of_year - 94) / 365 * 360));
  667. # Matzerakis 2010, eq. (3) .. (10)
  668. my $z = 90 - $alt;
  669. my $f = exp(-0.027 * $pabs / 1013.2 * $t_l / cosD($z));
  670. my $G_0 = 0.84 * $I_0_d * cosD($z) * $f;
  671. my $m_R0 = 1.0 / (sinD($alt) + 0.50572 * ($alt + 6.07995)**-1.6364);
  672. my $delta_R0 = 1.0 / (0.9 * $m_R0 + 9.4);
  673. my $tau = exp(-$t_l * $delta_R0 * $m_R0 * $pabs / 1013.2);
  674. my $I_d = $I_0 * $tau;
  675. my $I_h = $I_d * cosD($z);
  676. my $x = ($G_0 - $I_h);
  677. # anisotropic radiation is =! 0 only if the sun is visible
  678. # so multiply with $f_direct
  679. my $D_aniso = $x * $tau * $f_direct;
  680. my $D_iso = $x * (1.0 - $tau) * $f_svf;
  681. my $D_0 = $D_aniso + $D_iso;
  682. my $D_8 = 0.28 * $G_0 * $f_svf;
  683. Log(4, sprintf("D_aniso: %4.1f, D_iso: %4.1f, D_0: %4.1f, D_8: %4.1f", $D_aniso, $D_iso, $D_0, $D_8));
  684. return ($G_0, $I_h, $I_d, $D_0, $D_8);
  685. }
  686. # long wave surface radiation
  687. sub L_surface($$$$)
  688. {
  689. my ($Ta, $va, $G, $A) = @_;
  690. my $eps = 0.8;
  691. # Matzerakis 2010, eq. (12), (13)
  692. # iterate for surface temperature, (converges rapidly)
  693. my $Ts = $Ta;
  694. foreach my $i (1..10) {
  695. my $Ts_o = $Ts;
  696. my $E = $eps * $sigma * ($Ts + 273)**4 + (1 - $eps) * $A;
  697. my $Q = $sw_abs * $G + $A - $E;
  698. my $B = ($Q >= 0) ? -0.19 * $Q : -0.32 * $Q;
  699. $Ts = $Ta + ($Q + $B) / ((6.2 + 4.2 * $va) * (1 + 1 / $bowen));
  700. Log(5, sprintf("E = %4.1f, Q = %4.1f, B = %4.1f, Ts = %5.2f", $E, $Q, $B, $Ts));
  701. last if abs($Ts_o - $Ts) < 0.05;
  702. }
  703. # Radiation from surface
  704. my $E = $eps * $sigma * ($Ts + 273)**4 + (1 - $eps) * $A;
  705. return ($E, $Ts);
  706. }
  707. # compute T_tmrt, mean radiant temperature
  708. sub T_RMT($$$$$$)
  709. {
  710. my ($I_d_n, $D, $LW, $E, $alt, $f_direct) = @_;
  711. # Soil reflectance
  712. my $Rsoil = 0.2;
  713. # projection factor for cylindrical body
  714. my $fp = 0.308 * cosD($alt * (1 - $alt * $alt / 48402));
  715. # upper hemisphere
  716. my $R = 0.5 * ($LW + $sw_abs / $lw_abs * $D);
  717. # lower hemisphere
  718. $R += 0.5 * $sw_abs / $lw_abs * ($I_d_n * sinD($alt) + $D) * $Rsoil;
  719. $R += 0.5 * $E;
  720. my $T_mrt_ambient = ($R / $sigma)**0.25 - 273;
  721. # direct
  722. my $R_d = $sw_abs / $lw_abs * $fp * $I_d_n;
  723. my $T_mrt_full = (($R + $R_d)/ $sigma)**0.25 - 273;
  724. my $T_mrt = (($f_direct * $R_d + $R) / $sigma)**0.25 - 273;
  725. Log(4, sprintf("T_mrt_ambient: %4.1f, T_mrt_full: %4.1f, T_mrt: %4.1f", $T_mrt_ambient, $T_mrt_full, $T_mrt));
  726. return $T_mrt;
  727. }
  728. # some defaults for stand alone testing
  729. our $LAT = 50.138804;
  730. our $LON = 8.501993;
  731. our $altitude = 146;
  732. # should be callable outside of FHEM for development purposes
  733. sub _mean_radiant_temperature($$$$$$$$$$)
  734. {
  735. my ($azimuth, $alt, $Ta, $Hr, $va, $pabs, $f_direct, $Isensor, $coverage, $T_sky) = @_;
  736. my $ehPa = es($Ta) * $Hr * 0.01;
  737. Log(4, sprintf("Isensor: %3.1f, Pa %3.1f", $Isensor, $ehPa));
  738. Log(4, sprintf("Elevation: %3.1f, Azimuth: %3.1f", $alt, $azimuth));
  739. my ($G_0, $I_d, $I_h, $D_0, $D_8);
  740. if ($alt >= 5) {
  741. # get radiation values with f_svf_fl for 'feels like' temperature
  742. ($G_0, $I_h, $I_d, $D_0, $D_8) = radiation($alt, $pabs, $f_svf_fl, $f_direct);
  743. } elsif (0 < $alt && $alt < 5) {
  744. # twilight mode
  745. $I_d = $I_h = 0.0;
  746. $D_0 = $D_8 = $Isensor;
  747. $coverage = 0.5;
  748. } else {
  749. # night mode
  750. $I_d = $I_h = $D_0 = $D_8 = 0.0;
  751. $coverage = 0.75;
  752. }
  753. Log(4, sprintf("Coverage used for computation: %3.2f", $coverage));
  754. # compute coverage dependent values
  755. # direct radiation, normal
  756. my $I_d_n = $I_d * (1 - $coverage);
  757. # direct radiation, horizontal
  758. my $I_h_n = $I_h * (1 - $coverage);
  759. # diffuse radiation, Matzerakis 2010, eq. (7)
  760. my $D = $D_0 + ($D_8 - $D_0) * $coverage;
  761. # for very thick clouds everything measured is diffuse
  762. $D = $Isensor if ($D > $Isensor);
  763. my $I_lw;
  764. if (defined($T_sky)) {
  765. $I_lw = $sigma * ($T_sky + 273)**4;
  766. Log(4, sprintf("Direct_n: %4.0f, Diffuse: %4.0f Lw: %4.0f T_sky (IR): %4.1f", $I_d_n, $D, $I_lw, $T_sky));
  767. } else {
  768. # air long wave radiation, Matzerakis 2010, eq. (11)
  769. $I_lw = $sigma * (273 + $Ta)**4 * (0.82 -0.25 * 10**(-0.0945 * $ehPa)) * (1 + 0.21 * $coverage**2.5);
  770. # sky temperature just for reference
  771. my $T_s = ($I_lw / $sigma)**0.25 - 273;
  772. Log(4, sprintf("Direct_n: %4.0f, Diffuse: %4.0f Lw: %4.0f T_sky (comp): %4.1f", $I_d_n, $D, $I_lw, $T_s));
  773. }
  774. # Shouldn't that be multiplied by sky_view ?
  775. # surface long wave radiation
  776. my ($E, $Tsoil) = L_surface($Ta, $va, $I_h_n + $D, $I_lw);
  777. Log(4, sprintf("Tsoil = %4.1f, E = %4.1f", $Tsoil, $E));
  778. my $T_mrt = T_RMT($I_d_n, $D, $I_lw, $E, $alt, $f_direct);
  779. # ER = 'absorbed extra radiation' for apparent temperature
  780. # 0.725 = effective radiating part of body, 6 = radiating heat xfer coefficient
  781. my $ER = 0.725 * 6 * ($T_mrt - $Ta);
  782. return ($T_mrt, $ER);
  783. }
  784. # compute coverage by analysis of Isensor's time series
  785. # General method:
  786. # - compute (correction factor adjusted) ratio of actual radiation to full radiation
  787. # called 'normalized radiation, nr_..' ideally in (0,1)
  788. # - store nr_.. values in array
  789. # - count points in for bands of interval (0,1)
  790. # - derive coverage by from relative ratios of bands
  791. # - allow adjustment using e.g. an IR thermometer by calling an optional callback
  792. #
  793. sub compute_coverage($$$$$$)
  794. {
  795. my ($hash, $az, $alt, $pabs, $Isensor, $cb) = @_;
  796. my $max_nr_history = 225; # 60 minutes
  797. # create the device's nr_history array
  798. if (!exists($hash->{helper}{nr_history})) {
  799. $hash->{helper}{nr_history} = [];
  800. }
  801. my $nr_history = $hash->{helper}{nr_history};
  802. my ($alpha, $nr_coverage, $nr_stddev, $nr_band_0, $nr_band_1, $nr_band_2, $nr_band_3);
  803. my $name = $hash->{NAME};
  804. my $sr_invalid = 0;
  805. my $sun_visible = 0;
  806. $hash->{helper}{nr_current} = undef;
  807. # check whether sun is visible
  808. my @sv = split(/, */, main::AttrVal($name, "sunVisibility", "0,5,360"));
  809. my $az_low = shift(@sv);
  810. while (@sv > 1) {
  811. my $alt_min = shift(@sv);
  812. my $az_high = shift(@sv);
  813. if ($az_low <= $az && $az < $az_high) {
  814. $sun_visible = $alt >= $alt_min ? 1 : 0;
  815. last;
  816. }
  817. $az_low = $az_high;
  818. }
  819. Log(4, "sun_visible: $sun_visible");
  820. if ($alt >= 5 && $sun_visible) {
  821. Log(4, sprintf("compute_coverage: Isensor: %3.1f, pabs: %3.1f", $Isensor, $pabs));
  822. Log(4, sprintf("az: %3.1f, alt: %3.1f", $az, $alt));
  823. # get radiation with f_svf for sensor
  824. my ($G_0, $I_h, $I_d, $D_0, $D_8) = radiation($alt, $pabs, $f_svf_sensor, 1.0);
  825. Log(4, sprintf("G_0: %4.0f, I_d: %5.1f, I_h: %5.1f, D_0: %4.0f, D_8: %4.0f",
  826. $G_0, $I_d, $I_h, $D_0, $D_8));
  827. my $sday; # distance from June 21 in days
  828. my $day_of_year = (gmtime)[7];
  829. if ($day_of_year > 344) {
  830. $sday = (365 - $day_of_year) - 172;
  831. } else {
  832. $sday = $day_of_year - 172;
  833. }
  834. $sday = -$sday if ($sday < 0);
  835. my $sensor_type = main::AttrVal($name, "sensorType", "wh2601");
  836. if ($sensor_type eq "wh2601") {
  837. # apply sensor specific corrections
  838. $alpha = sensor_factor($az, $sday);
  839. Log(4, sprintf("sday: $sday, alpha: %3.2f", $alpha));
  840. # likely to be 100% wrong if out of range
  841. my $amin = main::AttrVal($name, "alphaMin", $alpha_min);
  842. my $amax = main::AttrVal($name, "alphaMax", $alpha_max);
  843. $amin = $amin > $alpha_min ? $amin : $alpha_min;
  844. $amax = $amax < $alpha_max ? $amax : $alpha_max;
  845. if ($alpha > $amax || $alpha < $amin
  846. || $az < $azimuth_min || $az > $azimuth_max) {
  847. $sr_invalid = 1;
  848. Log(4, "Alpha or sun's position is out of range");
  849. }
  850. } else {
  851. $alpha = 1.0;
  852. $sr_invalid = 0;
  853. }
  854. if (! $sr_invalid) {
  855. # use last coverage computed as estimation
  856. my $last_coverage = $hash->{helper}{coverage};
  857. $last_coverage = defined($last_coverage) ? $last_coverage : 0.5;
  858. my $D = (1 - $last_coverage) * $D_0 + $last_coverage * $D_8;
  859. my $I_h_m = $alpha * ($Isensor - $D);
  860. my $x = $G_0 - $D;
  861. my $nr = $I_h_m / $x;
  862. $hash->{helper}{nr_current} = $nr;
  863. Log(4, sprintf("I_h_m: %3.1f, Limit: %3.1f, nr: %3.2f", $I_h_m, $x, $nr));
  864. my $n_nr_history = push(@$nr_history, $nr);
  865. while ($n_nr_history > $max_nr_history) {
  866. shift(@$nr_history);
  867. $n_nr_history--;
  868. }
  869. # at least ~ 10 minutes of data
  870. if ($n_nr_history > 40) {
  871. $nr_band_0 = $nr_band_1 = $nr_band_2 = $nr_band_3 = 0;
  872. foreach my $nr (@$nr_history) {
  873. if ($nr > 0.8) {
  874. $nr_band_0++;
  875. } elsif (0.6 < $nr) {
  876. $nr_band_1++;
  877. } elsif (0.2 < $nr) {
  878. $nr_band_2++;
  879. } else {
  880. $nr_band_3++;
  881. }
  882. }
  883. $nr_band_0 /= $n_nr_history;
  884. $nr_band_1 /= $n_nr_history;
  885. $nr_band_2 /= $n_nr_history;
  886. $nr_band_3 /= $n_nr_history;
  887. # if 100% clear force coverage to be rounded down for oktas
  888. if ($nr_band_0 == 1) {
  889. $nr_coverage = 0.05;
  890. } else {
  891. # meaning:
  892. # for alternate high and med it's statistical
  893. # for 100% low it will be rounded up to OVC
  894. $nr_coverage = $nr_band_0 * 0.5/8 + $nr_band_1 * 3.5/8
  895. + $nr_band_2 * 6.5/8 + $nr_band_3 * 7.5/8;
  896. }
  897. $hash->{helper}{nr_coverage_raw} = $nr_coverage;
  898. # experimental stuff for low coverage
  899. # around 30 minutes
  900. my $n_sample = 120;
  901. if ($n_nr_history > $n_sample) {
  902. my $sum = 0;
  903. my $sum2 = 0;
  904. foreach my $i (($n_nr_history-$n_sample)..($n_nr_history-1)) {
  905. my $x = $nr_history->[$i];
  906. $sum += $x;
  907. $sum2 += $x * $x;
  908. }
  909. $sum /= $n_sample;
  910. $sum2 /= $n_sample;
  911. $nr_stddev = sqrt($sum2 - $sum * $sum);
  912. if ($nr_coverage <= 4/8 && $nr_stddev > 0.2) {
  913. $nr_coverage = 4/8;
  914. } elsif ($nr_coverage < 1/8) {
  915. if ($nr_stddev >= 0.1) {
  916. $nr_coverage = 2/8;
  917. } elsif ($nr_stddev > 0.015) {
  918. $nr_coverage = 1/8;
  919. }
  920. }
  921. }
  922. }
  923. }
  924. }
  925. # reset history on obstruction
  926. if (!$sun_visible || $sr_invalid) {
  927. @{$nr_history} = ();
  928. $nr_coverage = undef;
  929. }
  930. my $T_sky;
  931. # call the coverage callback
  932. my $coverage = $nr_coverage;
  933. if (defined($cb)) {
  934. Log(4, "cb: calling with coverage: " . (defined($coverage) ? $coverage : 'undef'));
  935. no strict "refs";
  936. eval {
  937. ($coverage, $T_sky) = &{"main::$cb"}($hash, $az, $alt, $coverage);
  938. };
  939. if ($@) {
  940. Log(0, "Eval of $cb failed: $@");
  941. }
  942. use strict "refs";
  943. Log(4, "cb: return coverage: " . (defined($coverage) ? $coverage : 'undef')
  944. . ", T_Sky: " . ($T_sky || 'undef'));
  945. }
  946. # store in helper so we can inspect it with list or create readings
  947. $hash->{helper}{nr_band_0} = $nr_band_0;
  948. $hash->{helper}{nr_band_1} = $nr_band_1;
  949. $hash->{helper}{nr_band_2} = $nr_band_2;
  950. $hash->{helper}{nr_band_3} = $nr_band_3;
  951. $hash->{helper}{sr_invalid} = $sr_invalid;
  952. $hash->{helper}{sun_visible} = $sun_visible;
  953. $hash->{helper}{alpha} = $alpha;
  954. $hash->{helper}{nr_stddev} = $nr_stddev;
  955. $hash->{helper}{nr_coverage} = $nr_coverage;
  956. $hash->{helper}{coverage} = $coverage;
  957. $coverage = defined($coverage) ? $coverage : 0.5;
  958. return ($coverage, $T_sky);
  959. }
  960. ##########################################################################################################
  961. # functions below are "public"
  962. package main;
  963. sub mean_radiant_temperature($$$$$$)
  964. {
  965. my ($devname, $Ta, $Hr, $va, $p, $Isensor) = @_;
  966. my $hash = $main::defs{$devname};
  967. if (!defined($hash)) {
  968. Log(1, 'feels_like: $devname is invalid');
  969. return undef;
  970. }
  971. $feels_like::LAT = AttrVal("global", "latitude", undef);
  972. $feels_like::LON = AttrVal("global", "longitude", undef);
  973. $feels_like::altitude = AttrVal("global", "altitude", undef);
  974. Log(1, 'feels_like needs global attributes "latitude", "longitude", and "altitude"')
  975. unless (defined($LAT) && defined($LON) && defined($altitude));
  976. # set tweakable constants
  977. my $x = AttrVal($devname, "sensorSkyViewFactor", undef);
  978. $feels_like::f_svf_sensor = $x if (defined($x));
  979. $x = AttrVal($devname, "skyViewFactor", undef);
  980. $feels_like::f_svf_fl = $x if (defined($x));
  981. $x = AttrVal($devname, "bowenRatio", undef);
  982. $feels_like::bowen = $x if (defined($x));
  983. $x = AttrVal($devname, "linkeTurbity", undef);
  984. if (defined($x)) {
  985. my @y;
  986. $x = "\@y = $x";
  987. eval($x);
  988. if ($@) {
  989. Log(1, "Invalid syntax for linkeTurbity -> $@");
  990. } else {
  991. @feels_like::T_L = @y;
  992. }
  993. }
  994. # part of direct radiation that should be used for T_mrt
  995. my $f_direct = AttrVal($devname, "directRadiationRatio", 0.4);
  996. my ($az, $alt) = feels_like::sun_position($LAT, $LON, time());
  997. $alt = 0 if ($alt < 0);
  998. readingsBulkUpdate($hash, "azimuth", round($az, 1));
  999. readingsBulkUpdate($hash, "altitude", round($alt, 1));
  1000. my $pabs = $p - $altitude / 8;
  1001. my $cb = AttrVal($devname, "coverageCallback", undef);
  1002. my ($coverage, $T_sky) = feels_like::compute_coverage($hash, $az, $alt, $pabs, $Isensor, $cb);
  1003. my @res = feels_like::_mean_radiant_temperature($az, $alt, $Ta, $Hr, $va, $p,
  1004. $f_direct, $Isensor, $coverage, $T_sky);
  1005. return (@res);
  1006. }
  1007. ##########################
  1008. sub T_UTCI($$$$$)
  1009. {
  1010. my ($Ta, $Hr, $va, $p, $T_mrt) = @_;
  1011. my $ehPa = feels_like::es($Ta) * $Hr * 0.01;
  1012. # UTCI seems to heavily overestimate for very low wind speeds ($va >= 0.5 is already implied in UTCI_approx)
  1013. if ($va < 2.0) {
  1014. $va = 1.0 + 0.5 * $va;
  1015. }
  1016. return feels_like::UTCI_approx($Ta, $ehPa, $T_mrt, $va);
  1017. }
  1018. sub T_apparent_temperature($$$$)
  1019. {
  1020. my ($Ta, $Hr, $va, $ER) = @_;
  1021. return feels_like::apparent_temperature($Ta, $Hr, $va, $ER);
  1022. }
  1023. 1;
  1024. =pod
  1025. =item helper
  1026. =item summary compute a 'feels like' temperature according to UTCI or AT and cloud coverage
  1027. =begin html
  1028. <a name="feels_like"></a>
  1029. <h3>feels_like</h3>
  1030. <ul>
  1031. Computation of a 'feels like' temperature based on ambient temperature, humidity, wind speed and solar radiation.<br>
  1032. While for a simple computation temperature, humidity, wind speed are sufficient this module is specifically tailored
  1033. to support stations with the Fine Offset sensor array marketed under several names like<br>
  1034. <ul>
  1035. <br>
  1036. <li>Ambient Weather WS-1200 / IP-1400 / ...</li>
  1037. <li>Froggit WS2601</li>
  1038. <li>Renkforce WH2600</li>
  1039. <br>
  1040. </ul>
  1041. Most of them are supported by the module <a href="commandref.html#HP1000">HP1000</a>.<br>
  1042. <br>
  1043. Algorithms providing a 'feels like' temperature work the following way:
  1044. <ul>
  1045. <li>Compute the <i>net extra radiation</i> on the human body using environmental data, the position of the sun and the amount of cloud cover<br>
  1046. resulting in the <a href = "https://en.wikipedia.org/wiki/Mean_radiant_temperature">Mean Radiant Temperature</a></li>
  1047. <li>Make a complete heat balance of the human body including this extra radiation.</li>
  1048. <li>Compare this to the heat balance of a walking person in the shadow with no wind and average humidity.</li>
  1049. <li>The temperature where these two match defines the <i>feels like</i>, <i>equivalent</i>, or <i>apparent</i> temperature.</li>
  1050. </ul>
  1051. </ul>
  1052. <ul>
  1053. This implementation computes the short and long wave radiation parts based on the position of the sun.<br>
  1054. Taking into account the <i>solar radiation reading</i> the amount of cloud cover is computed in <a href="https://en.wikipedia.org/wiki/Okta">Oktas</a>.<br>
  1055. Using this a <i>Mean Radiant Temperature</i> is computed that is consistent with the provided <i>solar radiation reading</i>.<br><br>
  1056. Currently this module provides two different <i>equivalent temperatures</i>:<br>
  1057. <ul>
  1058. <li>
  1059. <b><a href="http://www.utci.org">Universal Temperature Climate Index</a></b><br>
  1060. Currently considered as the gold standard. It was created 2007 by a joint effort of world wide experts.
  1061. See the pdf <a href="http://www.utci.org/utci_poster.pdf">Poster</a> for a quick introduction.<br>
  1062. </li>
  1063. <li>
  1064. <b><a href="http://www.bom.gov.au/info/thermal_stress/#apparent">Apparent Temperature</a></b><br>
  1065. An older standard developed by R. B. Steadman in the 1970s to 1990s and still in use in Australia and USA.<br>
  1066. </li>
  1067. </ul>
  1068. </ul>
  1069. <a name="feels_likedefine"></a>
  1070. <b>Define</b>
  1071. <ul>
  1072. <code>define &lt;name&gt; feels_like &lt;out-device&gt; &lt;temperature&gt; &lt;humidity&gt; [&lt;wind_speed&gt; [&lt;solarradiation&gt; [&lt;pressure&gt;]]]</code><br>
  1073. <br>
  1074. <ul>
  1075. Calculates a 'feels like' temperatures for specified readings and writes them into new readings for device &lt;out-device&gt;.<br>
  1076. Input readings can be specified as &lt;device&gt;:&lt;reading&gt. At least one input reading must belong to &lt;out-device&gt;.<br><br>
  1077. <i>Input Readings</i>
  1078. <table>
  1079. <tr><td>temperature</td> <td>ambient temperature [&deg;C]</td></tr>
  1080. <tr><td>humidity</td> <td>ambient relative humidity [%]</td></tr>
  1081. <tr><td>wind_speed</td> <td>speed of wind 10 m above ground (so be sure to calibrate accordingly) [m/s]</td></tr>
  1082. <tr><td>solarradiation&nbsp;</td> <td>solar radiation [W/m^2]</td></tr>
  1083. <tr><td>pressure</td> <td>pressure [hPa]</td></tr>
  1084. </table><br><br>
  1085. <i>Generated Readings</i>
  1086. <table>
  1087. <tr><td>temperature_utci</td> <td>temperature according to UTCI [&deg;C]</td></tr>
  1088. <tr><td>temperature_at</td> <td>temperature according to Steadman [&deg;C]</td></tr>
  1089. <tr><td>temperature_mrt</td> <td>mean radiant temperature [&deg;C]</td></tr>
  1090. <tr><td>extra_radiation</td> <td>extra radiation on human body [W/m^2]</td></tr>
  1091. </table><br><br>
  1092. If &lt;solarradiation&gt; is specified as input parameter these readings are generated in addition:<br><br>
  1093. <table>
  1094. <tr><td>coverage</td> <td>cloud cover as value 0.0 -> 1.0</td></tr>
  1095. <tr><td>oktas</td> <td>cloud cover in oktas 1 -> 8</td></tr>
  1096. <tr><td>clouds</td> <td>letter code for coverage SKC -> OVC</td></tr>
  1097. </table><br>
  1098. The value for <i>clouds</i> is compatible with Wunderground and can be uploaded with field code "clouds".<br><br>
  1099. <b>Note:</b>For cloud detection it is essential that an event for &lt;solarradiation&gt; is generated for each transmission
  1100. of the weather station, i.e. each 16 seconds.
  1101. </ul>
  1102. <br>
  1103. Example:<PRE>
  1104. # Use full functionality for a wh2601 weather station
  1105. define wh2601_fl feels_like wh2601 temperature humidity wind_speed solarradiation pressure
  1106. # Use different sensors
  1107. define Thermo_1_fl feels_like Thermo_1 temperature Thermo_2:humidity WS_Sensor:wind_speed
  1108. </PRE>
  1109. </ul>
  1110. <ul>
  1111. <br>
  1112. <b>Attributes</b><br>
  1113. <table>
  1114. <tr><td>latitude, longitude, altitude &nbsp;</td> <td>required</td> <td>must be defined in <code>global</code></td></tr>
  1115. <tr><td>maxTimediff</td> <td>optional, should match installation &nbsp;</td> <td>Maximum allowed time difference in seconds for readings of different sensors to be considered coherent (default: 600)</td></tr>
  1116. <tr><td>sensorSkyViewFactor</td> <td>optional, should match installation &nbsp;</td> <td>Fraction of sky that is visible to the sensor (default: 0.6)</td></tr>
  1117. <tr><td>skyViewFactor</td> <td>optional, your personal choice</td> <td>Fraction of visible sky to be used for computation of T_mrt (default: 0.7)</td></tr>
  1118. <tr><td>directRadiationRatio</td> <td>optional, your personal choice</td> <td>Fraction of direct insolation to be used for computation of T_mrt (default 0.4)</td></tr>
  1119. <tr><td>utciWindCutoff</td> <td>optional, your personal choice</td> <td>Max wind speed for UTCI computation (default: 3 m/s)</td></tr>
  1120. <tr><td>bowenRatio</td> <td>optional, technical</td> <td><a href="https://en.wikipedia.org/wiki/Bowen_ratio">Bowen ratio</a> of environment (default 0.6 = grassland)</td></tr>
  1121. <tr><td>linkeTurbity</td> <td>optional, technical</td> <td><a href="http://www.soda-pro.com/help/general-knowledge/linke-turbidity-factor">Linke Turbity</a> of atmosphere.<br>
  1122. Value should be an array of 12 values ( = one value / month) (default: values for Frankfurt)<br>
  1123. You can find values for other locations <a href="http://www.soda-pro.com/web-services/atmosphere/turbidity-linke-2003">here</a></td></tr>
  1124. <tr><td>sunVisibility</td> <td>optional, should match installation</td> <td>Comma separated list of azimuth/altitude values to describe whether the sun is visible (e.g. due to buildings, trees, ...).<br>Must start with 0 and end with 360.<br>
  1125. E.g. 0,5,75,20.5,130,10,360 -> sun must be > 5° between 0 and 75° azimuth, then > 20.5° between 75° and 130° etc.</td></tr>
  1126. <tr><td>coverageCallback</td> <td>optional, technical</td> <td>Callback function for use with an additional infrared thermometer</td></tr>
  1127. <tr><td>sensorType</td> <td>optional, technical</td> <td>Type of sensor for applying specific corrections, values:wh2601,generic (default: wh2601)</td></tr>
  1128. </table>
  1129. <br><br>
  1130. If your environment is a<br>
  1131. large paved parking lot use <code>skyViewFactor = 1.0, directRadiationRatio = 1.0, bowenRatio = 5</code>,<br>
  1132. shadowy wetland use <code>skyViewFactor = 0.2, directRadiationRatio = 0.2, bowenRatio = 0.2</code>,<br>
  1133. suburb, golf course... use the defaults.
  1134. </ul>
  1135. <br>
  1136. <ul>
  1137. <b>Bibliography</b><br>
  1138. P. Br&ouml;de et al. (2010): The Universal Thermal Climate Index UTCI in Operational Use<br>
  1139. Proceedings of Conference: Adapting to Change: New Thinking on Comfort Cumberland
  1140. Lodge, Windsor, UK, 9-11 April 2010
  1141. <a href="http://www.utci.org/isb/documents/windsor_vers05.pdf">download</a>
  1142. <br><br>
  1143. Andreas Matzarakis, Frank Rutz, Helmut Mayer (2010): Modelling radiation fluxes in simple and complex
  1144. environments: basics of the RayMan model<br>
  1145. Int J Biometeorol (2010) 54:131–139 <a href="http://www.urbanclimate.net/rayman/rayman12.zip">download</a>
  1146. <br><br>
  1147. Robert G. Steadman. (1994): Norms of apparent temperature in Australia.<br>
  1148. Aust. Met. Mag., Vol 43, 1-16. <a href="http://reg.bom.gov.au/amm/docs/1994/steadman.pdf">download</a>
  1149. <br><br>
  1150. John L. Monteith† and Mike H. Unsworth: Principles of Environmental Physics, Fourth Edition<br>
  1151. Elsevier, Kidlington, UK, 2013
  1152. </ul>
  1153. =end html
  1154. =cut