############################################################################### # $Id$ package main; use strict; use warnings; # use locale; use POSIX; use Data::Dumper; use utf8; use Encode qw(encode_utf8 decode_utf8); use UConv; # only to suppress file reload error in FHEM sub Unit_Initialize() { } # scale helper for metric numbers my $scales_m = { autoscale => { '0' => { format => '%i', scale => 1000, }, '0.001' => { format => '%i', scale => 1000, }, '0.1' => { format => '%.1f', scale => 1, }, '10' => { format => '%i', scale => 1, }, '1.0e3' => { format => '%.1f', scale => 0.001, }, '2.0e3' => { format => '%i', scale => 0.001, }, '1.0e6' => { format => '%.1f', scale => 0.001, }, '2.0e6' => { format => '%i', scale => 0.001, }, }, '1.0e-12' => { 'scale_txt_m' => 'p', 'scale_txt_long_m' => { de => 'Piko', en => 'Pico', }, }, '1.0e-9' => { 'scale_txt_m' => 'n', 'scale_txt_long_m' => { de => 'Nano', en => 'Nano', }, }, '1.0e-6' => { 'scale_txt_m' => 'μ', 'scale_txt_long_m' => { de => 'Mikro', en => 'Micro', }, }, '1.0e-3' => { 'scale_txt_m' => 'm', 'scale_txt_long_m' => { de => 'Milli', en => 'Mili', }, }, '1.0e-2' => { 'scale_txt_m' => 'c', 'scale_txt_long_m' => { de => 'Zenti', en => 'Centi', }, }, '1.0e-1' => { 'scale_txt_m' => 'd', 'scale_txt_long_m' => { de => 'Dezi', en => 'Deci', }, }, '1.0e0' => { 'scale_txt_m' => '', 'scale_txt_long_m' => '', }, '1.0e1' => { 'scale_txt_m' => 'da', 'scale_txt_long_m' => { de => 'Deka', en => 'Deca', }, }, '1.0e2' => { 'scale_txt_m' => 'h', 'scale_txt_long_m' => { de => 'Hekto', en => 'Hecto', }, }, '1.0e3' => { 'scale_txt_m' => 'k', 'scale_txt_long_m' => { de => 'Kilo', en => 'Kilo', }, }, '1.0e6' => { 'scale_txt_m' => 'M', 'scale_txt_long_m' => { de => 'Mega', en => 'Mega', }, }, '1.0e9' => { 'scale_txt_m' => 'G', 'scale_txt_long_m' => { de => 'Giga', en => 'Giga', }, }, '1.0e12' => { 'scale_txt_m' => 'T', 'scale_txt_long_m' => { de => 'Tera', en => 'Tera', }, }, '1.0e15' => { 'scale_txt_m' => 'P', 'scale_txt_long_m' => { de => 'Peta', en => 'Peta', }, }, }; # scale helper for metric square numbers my $scales_sq = { 'scale_txt_sq' => chr(0x00B2), 'scale_txt_long_sq' => { de => 'Quadrat', en => 'Square', }, }; # scale helper for metric cubic numbers my $scales_cu = { 'scale_txt_cu' => chr(0x00B3), 'scale_txt_long_cu' => { de => 'Kubik', en => 'Cubic', }, }; # scale helper for time related numbers my $scales_t = { # second 's' => { 'scale_txt_t' => 's', 'scale_txt_long_t' => { de => 'Sekunde', en => 'second', }, 'scale_txt_long_pl_t' => { de => 'Sekunden', en => 'seconds', }, }, # minute 'min' => { 'scale_txt_t' => { de => 'Min', en => 'min', }, 'scale_txt_long_t' => { de => 'Minute', en => 'minute', }, 'scale_txt_long_pl_t' => { de => 'Minuten', en => 'minutes', }, }, # hour 'h' => { 'scale_txt_t' => 'h', 'scale_txt_long_t' => { de => 'Stunde', en => 'hour', }, 'scale_txt_long_pl_t' => { de => 'Stunden', en => 'hours', }, }, # day 'd' => { 'scale_txt_t' => 'd', 'scale_txt_long_t' => { de => 'Tag', en => 'day', }, 'scale_txt_long_pl_t' => { de => 'Tage', en => 'days', }, }, # week 'w' => { 'scale_txt_t' => 'w', 'scale_txt_long_t' => { de => 'Woche', en => 'week', }, 'scale_txt_long_pl_t' => { de => 'Wochen', en => 'weeks', }, }, # month 'm' => { 'scale_txt_t' => 'm', 'scale_txt_long_t' => { de => 'Monat', en => 'month', }, 'scale_txt_long_pl_t' => { de => 'Monate', en => 'month', }, }, # year 'a' => { 'scale_txt_t' => 'a', 'scale_txt_long_t' => { de => 'Jahr', en => 'year', }, 'scale_txt_long_pl_t' => { de => 'Jahre', en => 'years', }, }, }; # scale helper for time related numbers # Overall structure/grouping based on # https://de.wikipedia.org/wiki/Liste_physikalischer_Gr%C3%B6%C3%9Fen # # Scientific range: 0 - 99 # FHEM Builtin range: 900 - 998 # FHEM User Defined range: >= 999 # # rtype_base => reference to base rtype in $rtypes used for # automatic unit conversion my $rtype_base = { 0 => { dimension => 'L', formula_symbol => 'l', rtype_base => 'm', base_description => { de => 'Länge', en => 'length', }, format => '%.0f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 1 => { dimension => 'M', formula_symbol => 'm', rtype_base => 'kg', base_description => { de => 'Masse', en => 'mass', }, format => '%.0f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 2 => { dimension => 'T', formula_symbol => 't', rtype_base => 's', base_description => { de => 'Zeit', en => 'time', }, format => '%.0f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 3 => { dimension => 'I', formula_symbol => 'i', rtype_base => 'a', base_description => { de => 'elektrische Stromstärke', en => 'electric current', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 4 => { dimension => 'θ', formula_symbol => 'T', rtype_base => 'k', base_description => { de => 'absolute Temperatur', en => 'absolute temperature', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 5 => { dimension => 'N', formula_symbol => 'n', rtype_base => 'mol', base_description => { de => 'Stoffmenge', en => 'amount of substance', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 6 => { dimension => 'J', formula_symbol => 'Iv', rtype_base => 'cd', base_description => { de => 'Lichtstärke', en => 'luminous intensity', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 7 => { dimension => 'M L^2 T^−2', formula_symbol => 'E', rtype_base => 'j', base_description => { de => 'Energie', en => 'energy', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 8 => { dimension => 'T^−1', formula_symbol => 'f', rtype_base => 'hz', base_description => { de => 'Frequenz', en => 'frequency', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 9 => { dimension => 'M L^2 T^−3', formula_symbol => 'P', rtype_base => 'w', base_description => { de => 'Leistung', en => 'power', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 10 => { dimension => 'M L^−1 T^−2', formula_symbol => 'p', rtype_base => 'pa', base_description => { de => 'Druck', en => 'pressure', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 11 => { dimension => 'M L^−1 T^−2', formula_symbol => 'pabs', rtype_base => 'pabs', base_description => { de => 'absoluter Druck', en => 'absolute pressure', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 12 => { dimension => 'M L^−1 T^−2', formula_symbol => 'pamb', rtype_base => 'pamb', base_description => { de => 'Luftdruck', en => 'air pressure', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 13 => { dimension => 'M L^2 T^−3 I^−1', formula_symbol => 'U', rtype_base => 'v', base_description => { de => 'elektrische Spannung', en => 'electric voltage', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 14 => { dimension => '1', formula_symbol => '', rtype_base => 'rad', base_description => { de => 'ebener Winkel', en => 'plane angular', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 15 => { dimension => 'L T^−1', formula_symbol => 'v', rtype_base => 'kmph', base_description => { de => 'Geschwindigkeit', en => 'speed', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 16 => { dimension => 'L^−2 J', formula_symbol => 'Ev', rtype_base => 'lx', base_description => { de => 'Beleuchtungsstärke', en => 'illumination intensity', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 17 => { dimension => 'J', formula_symbol => 'F', rtype_base => 'lm', base_description => { de => 'Lichtstrom', en => 'luminous flux', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 18 => { dimension => 'L^3', formula_symbol => 'V', rtype_base => 'm3', base_description => { de => 'Volumen', en => 'volume', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 19 => { dimension => '1', formula_symbol => 'B', rtype_base => 'b', base_description => { de => 'Logarithmische Größe', en => 'logarithmic level', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 20 => { dimension => 'I T', formula_symbol => 'C', rtype_base => 'coul', base_description => { de => 'elektrische Ladung', en => 'electric charge', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 21 => { dimension => '', formula_symbol => 'F', rtype_base => 'far', base_description => { de => 'elektrische Kapazität', en => 'electric capacity', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 22 => { dimension => 'M L2 T−3 I−2', formula_symbol => 'R', rtype_base => 'ohm', base_description => { de => 'elektrischer Widerstand', en => 'electric resistance', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 23 => { dimension => 'L^2', formula_symbol => 'A', rtype_base => 'm2', base_description => { de => 'Flächeninhalt', en => 'surface area', }, format => '%i', decimal_mark => { de => 2, en => 1, }, scope => { minValue => 0 }, }, 24 => { base_description => { de => 'Währung', en => 'currency', }, format => '%.2f', decimal_mark => { de => 2, en => 1, }, scope => '^[0-9]*(?:\.[0-9]*)?$', }, 25 => { base_description => { de => 'Zahlen', en => 'numbering', }, format => '%.1f', decimal_mark => { de => 2, en => 1, }, tmpl => '%value%', }, 26 => { base_description => { de => 'Logische Operatoren', en => 'logical operators', }, tmpl => '%value%', }, 900 => { base_description => 'FHEM Builtin Readings Type', tmpl => '%value%', }, 999 => { base_description => 'FHEM User Defined Readings Type', tmpl => '%value%', }, }; # FHEM built-in reading types (RType). # Values will be combined into a new super-hash using # cross-references to $rtype_base, $scales_m, $scales_sq, $scales_cu, $scales_t # # ref_base => reference to $rtype_base id to include it's keys here # ref_sq => include keys from $scales_sq here; normally combined with a scale_sq reference as well # ref_cu => include keys from $scales_cu here; normally combined with a scale_cu reference as well # ref_t => reference to $scales_t id to include it's keys here; ; normally combined with a scale_t reference as well # ref => self-reference to $rtype id to include it's keys here (RType alias helper) my $rtypes = { # others url => { ref_base => 900, rtype_description => 'General URL including protocol prefix', tmpl => '%value%', }, url_http => { ref_base => 900, rtype_description => 'HTTP/S URL w/ or w/o protocol prefix', tmpl => '%value%', }, trend => { ref_base => 900, symbol => [ chr(0x2B0C), chr(0x2B08), chr(0x2B0A) ], txt => [ '=', '+', '-' ], txt_long => { de => [ 'gleichbleibend', 'steigend', 'fallend' ], en => [ 'steady', 'rising', 'falling' ], fr => [ 'stable', 'croissant', 'décroissant' ], nl => [ 'stabiel', 'stijgend', 'dalend' ], pl => [ 'stabilne', 'rośnie', 'spada' ], }, scope => [ '^(=|steady|stable|0)$', '^(\+|rising|up|1)$', '^(-|falling|down|2)$' ], tmpl => '%symbol%', tmpl_long => '%txt_long%', rtype_description => 'Trend', }, oknok => { ref_base => 900, txt => { de => [ 'Fehler', 'ok', 'Warnung' ], en => [ 'error', 'ok', 'warning' ], }, scope => [ '^(nok|error|dead|invalid|0)$', '^(ok|alive|valid|1)$', '^(warning|warn|low|2)$', '^(.*)$' ], rtype_description => { de => 'Fehlerstatus; siehe RType roknok, sofern 0<>1 vertauschte Bedeutung haben', en => 'error state', }, }, roknok => { ref_base => 900, txt => { de => [ 'Fehler', 'ok', 'Warnung' ], en => [ 'error', 'ok', 'warning' ], }, scope => [ '^(nok|error|dead|invalid|1)$', '^(ok|alive|valid|0)$', '^(warning|warn|low|2)$', '^(.*)$' ], rtype_description => { de => 'verdrehter Fehlerstatus, bei dem 0=ok und 1=Fehler bedeutet; Gegenteil von RType oknok', en => 'reversed error state', }, }, onoff => { ref_base => 900, txt => { de => [ 'aus', 'an', 'nicht verfügbar' ], en => [ 'off', 'on', 'absent' ], }, scope => [ '^(off|no|standby|0)$', '^(on|yes|1)$', '^(absent|offline|2)$', ], rtype_description => { de => 'Schaltstatus', en => 'Switch state', }, }, reachable => { ref_base => 900, txt => { de => [ 'nicht verfügbar', 'verfügbar' ], en => [ 'unavailable', 'available' ], }, scope => [ '^(unavailable|absent|disappeared|false|no|0)$', '^(available|present|appeared|true|yes|1)$' ], rtype_description => { de => 'Verfügbarkeit/Erreichbarkeit', en => 'availability/reachability', }, }, weekday => { ref_base => 900, symbol => { de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt => { de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt_long => { de => [ 'Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag' ], en => [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ], }, scope => { de => [ '^(So|Son|Sonntag|0|7)$', '^(Mo|Mon|Montag|1)$', '^(Di|Die|Dienstag|2)$', '^(Mi|Mit|Mittwoch|3)$', '^(Do|Don|Donnerstag|4)$', '^(Fr|Fre|Freitag|5)$', '^(Sa|Sam|Samstag|6)$', ], en => [ '^(Sun|Su|Sunday|0|7)$', '^(Mon|Mo|Monday|1)$', '^(Tue|Tu|Tuesday|2)$', '^(Wed|We|Wednesday|3)$', '^(Thu|Th|Thursday|4)$', '^(Fri|Fr|Friday|5)$', '^(Sat|Sa|Saturday|6)$' ], }, tmpl => '%txt%', tmpl_long => '%txt_long%', rtype_description => { de => 'Wochentag nach englisch-amerikanischer Annahme des Wochenstarts am Sonntag', en => 'Day of the week according to english assumption for the week to start on sunday', }, }, weekday_iso => { ref_base => 900, symbol => { de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt => { de => [ 'So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So' ], en => [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' ], }, txt_long => { de => [ 'Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag' ], en => [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ], }, scope => { de => [ '^(So|Son|Sonntag|7)$', '^(Mo|Mon|Montag|1)$', '^(Di|Die|Dienstag|2)$', '^(Mi|Mit|Mittwoch|3)$', '^(Do|Don|Donnerstag|4)$', '^(Fr|Fre|Freitag|5)$', '^(Sa|Sam|Samstag|6)$' ], en => [ '^(Sun|Su|Sunday|7)$', '^(Mon|Mo|Monday|1)$', '^(Tue|Tu|Tuesday|2)$', '^(Wed|We|Wednesday|3)$', '^(Thu|Th|Thursday|4)$', '^(Fri|Fr|Friday|5)$', '^(Sat|Sa|Saturday|6)$' ], }, tmpl => '%txt%', tmpl_long => '%txt_long%', rtype_description => { de => 'Wochentag nach ISO-Standard, Woche beginnend am Montag', en => 'Day of the week according to ISO standard, week beginning on Mondays', }, }, weekday_night => { ref_base => 900, symbol => { de => [ 'So N', 'Mo N', 'Di N', 'Mi N', 'Do N', 'Fr N', 'Sa N' ], en => [ 'Sun N', 'Mon N', 'Tue N', 'Wed N', 'Thu N', 'Fri N ', 'Sat N' ], }, txt => { de => [ 'So N', 'Mo N', 'Di N', 'Mi N', 'Do N', 'Fr N', 'Sa N' ], en => [ 'Sun N', 'Mon N', 'Tue N', 'Wed N', 'Thu N', 'Fri N ', 'Sat N' ], }, txt_long => { de => [ 'Sonntag Nacht', 'Montag Nacht', 'Dienstag Nacht', 'Mittwoch Nacht', 'Donnerstag Nacht', 'Freitag Nacht', 'Samstag Nacht' ], en => [ 'Sunday Night', 'Monday Night', 'Tuesday Night', 'Wednesday Night', 'Thursday Night', 'Friday Night', 'Saturday Night' ], }, scope => { de => [ '^(\s*(Nacht|Na)?\s*(So|Son|Sonntag|0)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Mo|Mon|Montag|1)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Di|Die|Dienstag|2)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Mi|Mit|Mittwoch|3)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Do|Don|Donnerstag|4)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Fr|Fre|Freitag|5)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Sa|Sam|Samstag|6)\s*(Nacht|Na)?\s*)$' ], en => [ '^(\s*(Night|Na)?\s*(Sun|Su|Sunday|0)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Mon|Mo|Monday|1)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Tue|Tu|Tuesday|2)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Wed|We|Wednesday|3)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Thu|Th|Thursday|4)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Fri|Fr|Friday|5)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Sat|Sa|Saturday|6)\s*(Night|Na)?\s*)$' ], }, tmpl => '%txt%', tmpl_long => '%txt_long%', rtype_description => { de => 'Nächtlicher Wochentag nach englisch-amerikanischem Standard des Wochenstarts am Sonntag', en => 'Nightly day of the week according to english standard for the week to start on sunday', }, }, weekday_night_iso => { ref_base => 900, symbol => { de => [ 'So N', 'Mo N', 'Di N', 'Mi N', 'Do N', 'Fr N', 'Sa N' ], en => [ 'Sun N', 'Mon N', 'Tue N', 'Wed N', 'Thu N', 'Fri N ', 'Sat N' ], }, txt => { de => [ 'So N', 'Mo N', 'Di N', 'Mi N', 'Do N', 'Fr N', 'Sa N' ], en => [ 'Sun N', 'Mon N', 'Tue N', 'Wed N', 'Thu N', 'Fri N ', 'Sat N' ], }, txt_long => { de => [ 'Sonntag Nacht', 'Montag Nacht', 'Dienstag Nacht', 'Mittwoch Nacht', 'Donnerstag Nacht', 'Freitag Nacht', 'Samstag Nacht' ], en => [ 'Sunday Night', 'Monday Night', 'Tuesday Night', 'Wednesday Night', 'Thursday Night', 'Friday Night', 'Saturday Night' ], }, scope => { de => [ '^(\s*(Nacht|Na)?\s*(So|Son|Sonntag|6)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Mo|Mon|Montag|0)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Di|Die|Dienstag|1)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Mi|Mit|Mittwoch|2)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Do|Don|Donnerstag|3)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Fr|Fre|Freitag|4)\s*(Nacht|Na)?\s*)$', '^(\s*(Nacht|Na)?\s*(Sa|Sam|Samstag|5)\s*(Nacht|Na)?\s*)$' ], en => [ '^(\s*(Night|Na)?\s*(Sun|Su|Sunday|6)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Mon|Mo|Monday|0)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Tue|Tu|Tuesday|1)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Wed|We|Wednesday|2)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Thu|Th|Thursday|3)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Fri|Fr|Friday|4)\s*(Night|Na)?\s*)$', '^(\s*(Night|Na)?\s*(Sat|Sa|Saturday|5)\s*(Night|Na)?\s*)$' ], }, tmpl => '%txt%', tmpl_long => '%txt_long%', rtype_description => { de => 'Nächtlicher Wochentag nach ISO-Standard, Woche beginnend am Montag', en => 'Nightly day of the week according to ISO standard, week beginning on Mondays', }, }, direction => { ref_base => 900, formula_symbol => 'Dir', ref => 'gon', scope => { minValue => 0, maxValue => 360 }, rtype_description => { de => 'Richtungsangabe', en => 'direction', }, }, compasspoint => { ref_base => 900, formula_symbol => 'CP', ref => 'gon', txt => { de => [ 'N', 'NNO', 'NO', 'ONO', 'O', 'OSO', 'SO', 'SSO', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], en => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], nl => [ 'N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW' ], fr => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' ], pl => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], }, txt_long => { de => [ 'Norden', 'Nord-Nordost', 'Nordost', 'Ost-Nordost', 'Osten', 'Ost-Südost', 'Südost', 'Süd-Südost', 'Süden', 'Süd-Südwest', 'Südwest', 'West-Südwest', 'Westen', 'West-Nordwest', 'Nordwest', 'Nord-Nordwest' ], en => [ 'North', 'North-Northeast', 'Northeast', 'East-Northeast', 'East', 'East-Southeast', 'Southeast', 'South-Southeast', 'South', 'South-Southwest', 'Southwest', 'West-Southwest', 'West', 'West-Northwest', 'Northwest', 'North-Northwest' ], nl => [ 'N', 'NNO', 'NO', 'ONO', 'O', 'OZO', 'ZO', 'ZZO', 'Z', 'ZZW', 'ZW', 'WZW', 'W', 'WNW', 'NW', 'NNW' ], fr => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSO', 'SO', 'OSO', 'O', 'ONO', 'NO', 'NNO' ], pl => [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW' ], }, scope => { de => [ { ge => 0.0, lt => 22.5, eq => 360, regex => '^(N|Norden|0)$', value_num => 0.0, }, { ge => 22.5, lt => 45.0, regex => '^(NNO|Nord-Nordost|1)$', value_num => 22.5, }, { ge => 45.0, lt => 67.5, regex => '^(NO|Nordost|2)$', value_num => 45.0, }, { ge => 67.5, lt => 90.0, regex => '^(ONO|Ost-Nordost|3)$', value_num => 67.5, }, { ge => 90.0, lt => 112.5, regex => '^(O|Osten|4)$', value_num => 90.0, }, { ge => 112.5, lt => 135.0, regex => '^(OSO|Ost-S(ü|u|ue)dost|5)$', value_num => 112.5, }, { ge => 135.0, lt => 157.5, regex => '^(SO|S(ü|u|ue)dost|6)$', value_num => 135.0, }, { ge => 157.5, lt => 180.0, regex => '^(SSO|S(ü|u|ue)d-S(ü|u|ue)dost|7)$', value_num => 157.5, }, { ge => 180.0, lt => 202.5, regex => '^(S|S(ü|u|ue)den|8)$', value_num => 180.0, }, { ge => 202.5, lt => 225.0, regex => '^(SSW|S(ü|u|ue)d-S(ü|u|ue)dwest|9)$', value_num => 202.5, }, { ge => 225.0, lt => 247.5, regex => '^(SW|S(ü|u|ue)dwest|10)$', value_num => 225.0, }, { ge => 247.5, lt => 270.0, regex => '^(WSW|West-S(ü|u|ue)dwest|11)$', value_num => 247.5, }, { ge => 270.0, lt => 292.5, regex => '^(W|Westen|12)$', value_num => 270.0, }, { ge => 292.5, lt => 315.0, regex => '^(WNW|West-Nordwest|13)$', value_num => 292.5, }, { ge => 315.0, lt => 337.5, regex => '^(NW|Nordwest|14)$', value_num => 315.0, }, { ge => 337.5, lt => 360, regex => '^(NNW|Nord-Nordwest|15)$', value_num => 337.5, }, ], en => [ { ge => 0.0, lt => 22.5, eq => 360, regex => '^(N|North|0)$', value_num => 0.0, }, { ge => 22.5, lt => 45.0, regex => '^(NNE|North-Northeast|1)$', value_num => 22.5, }, { ge => 45.0, lt => 67.5, regex => '^(NE|Northeast|2)$', value_num => 45.0, }, { ge => 67.5, lt => 90.0, regex => '^(ENE|East-Northeast|3)$', value_num => 67.5, }, { ge => 90.0, lt => 112.5, regex => '^(E|East|4)$', value_num => 90.0, }, { ge => 112.5, lt => 135.0, regex => '^(ESE|East-Southeast|5)$', value_num => 112.5, }, { ge => 135.0, lt => 157.5, regex => '^(SE|Southeast|6)$', value_num => 135.0, }, { ge => 157.5, lt => 180.0, regex => '^(SSE|South-Southeast|7)$', value_num => 157.5, }, { ge => 180.0, lt => 202.5, regex => '^(S|South|8)$', value_num => 180.0, }, { ge => 202.5, lt => 225.0, regex => '^(SSW|South-Southwest|9)$', value_num => 202.5, }, { ge => 225.0, lt => 247.5, regex => '^(SW|Southwest|10)$', value_num => 225.0, }, { ge => 247.5, lt => 270.0, regex => '^(WSW|West-Southwest|11)$', value_num => 247.5, }, { ge => 270.0, lt => 292.5, regex => '^(W|West|12)$', value_num => 270.0, }, { ge => 292.5, lt => 315.0, regex => '^(WNW|West-Northwest|13)$', value_num => 292.5, }, { ge => 315.0, lt => 337.5, regex => '^(NW|Northwest|14)$', value_num => 315.0, }, { ge => 337.5, lt => 360, regex => '^(NNW|North-Northwest|15)$', value_num => 337.5, }, ], }, rtype_description => { de => 'Himmelsrichtung', en => 'point of the compass', }, tmpl_long => '%txt_long%', }, closure => { ref_base => 900, txt => { de => [ 'geschlossen', 'offen', 'gekippt' ], en => [ 'closed', 'open', 'tilted' ], }, scope => [ '^(closed|0)$', '^(open|1)$', '^(tilted|2)$' ], rtype_description => { de => 'Status für Fenster und Türen', en => 'state for windows and doors', }, }, condition_weather => { ref_base => 900, txt => { de => [ 'klar', 'sonnig', 'bewölkt', 'Regen' ], en => [ 'clear', 'sunny', 'cloudy', 'rain' ], }, scope => [ '^((nt_)?clear|nt_sunny|0)$', '^(sunny|1)$', '^((nt_)?cloudy|2)$', '^((nt_)?rain|3)$' ], rtype_description => { de => 'Wetterbedingung', en => 'weather condition', }, }, condition_hum => { ref_base => 900, txt => { de => [ 'trocken', 'niedrig', 'optimal', 'hoch', 'feucht' ], en => [ 'dry', 'low', 'ideal', 'high', 'wet' ], }, scope => [ '^(dry|0)$', '^(low|1)$', '^(ideal|optimal|2)$', '^(high|3)$', '^(wet|4)$' ], rtype_description => { de => 'Feuchtigkeitsbedingung', en => 'humidity condition', }, }, condition_uvi => { ref_base => 900, txt => { de => [ 'niedrig', 'moderat', 'hoch', 'sehr hoch', 'extrem' ], en => [ 'low', 'moderate', 'high', 'very high', 'extreme' ], }, scope => [ '^(low|0)$', '^(moderate|1)$', '^(high|2)$', '^(veryhigh|3)$', '^(extreme|4)$' ], rtype_description => { de => 'UV Bedingung', en => 'UV condition', }, }, # color rgbhex => { ref_base => 900, rtype_description => { de => 'RGB Farbwert in Hex Notation', en => 'RGB color value in Hex notation', }, scope => '^#?(([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2}))$', }, rgb => { ref_base => 900, rtype_description => { de => 'RGB Farbwert in Dezimal Notation', en => 'RGB color value in decimal notation', }, scope => '^[\s\t\n ]*(?:rgb|RGB)?[\s\t\n ]*\(?[\s\t\n ]*((?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5])[\s\t\n ]*,?[\s\t\n ]*)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5])[\s\t\n ]*,?[\s\t\n ]*)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5])))[\s\t\n ]*\)?[\s\t\n ]*$', }, # logical operators bool => { ref_base => 26, txt => { de => [ 'falsch', 'wahr' ], en => [ 'false', 'true' ], }, scope => [ '^(false|n|no|0)$', '^(true|y|yes|1)$' ], rtype_description => { de => 'Boolesch wahr/falsch', en => 'Boolean true/false', }, }, yesno => { ref_base => 26, txt => { de => [ 'nein', 'ja' ], en => [ 'no', 'yes' ], }, scope => [ '^(no|n|false|0)$', '^(yes|y|true|1)$' ], rtype_description => { de => 'Boolesch ja/nein', en => 'Boolean yes/no', }, }, # decimal numbering short => { ref_base => 25, format => '%i', rtype_description => { de => 'Ganzzahl zwischen -32768 und 32767', en => 'Integer between -32768 and 32767', }, scope => { minValue => -32768, maxValue => 32767 }, }, rshort => { ref_base => 25, format => '%.0f', rtype_description => { de => 'gerundete Ganzzahl zwischen -32768 und 32767', en => 'rounded integer between -32768 and 32767', }, scope => { minValue => -32768, maxValue => 32767 }, }, long => { ref_base => 25, format => '%i', rtype_description => { de => 'Ganzzahl zwischen -2147483648 und 214748364', en => 'Integer between -2147483648 and 214748364', }, scope => { minValue => -2147483648, maxValue => 214748364 }, }, rlong => { ref_base => 25, format => '%.0f', rtype_description => { de => 'gerundete Ganzzahl zwischen -2147483648 und 214748364', en => 'rounded integer between -2147483648 and 214748364', }, scope => { minValue => -2147483648, maxValue => 214748364 }, }, integer => { ref_base => 25, format => '%i', rtype_description => { de => 'Ganzzahl', en => 'Integer', }, scope => '^([0-9]*(?:\.[0-9]*)?)$', }, rinteger => { ref_base => 25, format => '%.0f', rtype_description => { de => 'gerundete Ganzzahl', en => 'rounded integer', }, scope => '^([0-9]*(?:\.[0-9]*)?)$', }, float => { ref_base => 25, format => '%.2f', rtype_description => { de => 'Fließkommazahl', en => 'floating number', }, scope => '^([0-9]*(?:\.[0-9]*)?)$', }, pct => { ref_base => 25, format => '%i', symbol => chr(0x0025), suffix => 'pct', tmpl => '%value%' . chr(0x202F) . '%symbol%', tmpl_long => '%value% %txt%', txt => { de => 'Prozent', en => 'percent', }, scope => { minValue => 0, maxValue => 100 }, }, # binary numbering bin => { rtype_description => { de => 'Binärnummer', en => 'Binary number', }, scope => '^[01]+$', }, # octal numbering oct => { rtype_description => { de => 'Oktalnummer', en => 'Octal number', }, scope => '^[0-7]+$', }, # hexadecimal numbering hex => { rtype_description => { de => 'Hexadezimalnummer', en => 'Hexadecimal number', }, scope => '^[0-9a-fA-F]+$', }, # currency #https://en.wikipedia.org/wiki/Euro #https://en.wikipedia.org/wiki/Linguistic_issues_concerning_the_euro euro => { ref_base => 24, format => '%.2f', symbol => '€', suffix => 'EUR', txt => { de => 'Euro', en => 'euro', }, tmpl => { de => '%symbol%%value%', de_de => '%value%' . chr(0x202F) . '%symbol%', en => '%symbol%%value%', }, scope => '^([0-9]*(?:\.[0-9]*)?)$', }, pound_uk => { ref_base => 24, format => '%.2f', symbol => '£', suffix => 'GBP', txt => { de => 'Pfund', en => 'pound', }, txt_long => { de => 'Britisches Pfund', en => 'British pound', }, txt_long_pl => { de => 'Britische Pfund', en => 'British pound', }, tmpl => '%symbol%%value%', scope => '^([0-9]*(?:\.[0-9]*)?)$', }, dollar_us => { ref_base => 24, format => '%.2f', symbol => '$', suffix => 'USD', txt => { de => 'Dollar', en => 'dollar', }, txt_long => { de => 'US Dollar', en => 'US dollar', }, tmpl => '%symbol%%value%', scope => '^([0-9]*(?:\.[0-9]*)?)$', }, cent => { ref_base => 24, format => '%.0f', symbol => { de => 'ct', en => '¢', }, suffix => { de => 'ct', en => 'c', }, txt => { de => 'Cent', en => 'cent', }, tmpl => { de => '%value%%symbol%', de_de => '%value%' . chr(0x202F) . '%symbol%', en => '%value%%symbol%', }, scope => '^([0-9]*(?:\.[0-9]*)?)$', }, # plane angular gon => { ref_base => 14, symbol => chr(0x00B0), suffix => 'gon', txt => { de => 'Grad', en => 'gradians', }, tmpl => '%value%%symbol%', decimal_mark => { de => 2, en => 1, }, format => '%i', }, rad => { ref_base => 14, suffix => 'rad', txt => { de => 'Radiant', en => 'radiant', }, decimal_mark => { de => 2, en => 1, }, format => '%i', }, # temperature c => { ref_base => 4, symbol => chr(0x00B0) . 'C', suffix => 'C', txt => { de => 'Grad Celsius', en => 'Degree Celsius', }, txt_pl => { de => 'Grad Celsius', en => 'Degrees Celsius', }, scope => { minValue => -273.15 }, }, f => { ref_base => 4, symbol => chr(0x00B0) . 'F', suffix => 'F', txt => { de => 'Grad Fahrenheit', en => 'Degree Fahrenheit', }, txt_pl => { de => 'Grad Fahrenheit', en => 'Degrees Fahrenheit', }, scope => { minValue => -459.67 }, }, k => { ref_base => 4, suffix => 'K', txt => { de => 'Kelvin', en => 'Kelvin', }, }, # pressure bar => { ref_base => 10, scale_m => '1.0e0', suffix => 'bar', txt => { de => 'Bar', en => 'Bar', }, }, mbar => { ref => 'bar', scale_m => '1.0e-3', }, pa => { ref_base => 10, scale_m => '1.0e0', suffix => 'Pa', txt => { de => 'Pascal', en => 'Pascal', }, }, hpa => { ref => 'pa', scale_m => '1.0e2', }, pamb => { ref_base => 12, scale_m => '1.0e0', suffix => 'Pa', txt => { de => 'Pascal', en => 'Pascal', }, }, hpamb => { ref => 'pamb', scale_m => '1.0e2', }, inhg => { ref_base => 12, suffix => 'inHg', format => '%.2f', txt => { de => 'Zoll Quecksilbersäule', en => 'Inches of Mercury', }, }, mmhg => { ref_base => 12, suffix => 'mmHg', txt => { de => 'Millimeter Quecksilbersäule', en => 'Milimeter of Mercury', }, }, # length km => { ref => 'm', scale_m => '1.0e3', }, hm => { ref => 'm', scale_m => '1.0e2', }, dam => { ref => 'm', scale_m => '1.0e1', }, m => { ref_base => 0, scale_m => '1.0e0', suffix => 'm', txt => { de => 'Meter', en => 'meter', }, }, dm => { ref => 'm', scale_m => '1.0e-1', }, cm => { ref => 'm', scale_m => '1.0e-2', }, mm => { ref => 'm', scale_m => '1.0e-3', }, um => { ref => 'm', scale_m => '1.0e-6', }, nm => { ref => 'm', scale_m => '1.0e-9', }, pm => { ref => 'm', scale_m => '1.0e-12', }, fm => { ref => 'm', scale_m => '1.0e-15', }, in => { ref_base => 0, symbol => '″', suffix => 'in', txt => { de => 'Zoll', en => 'inch', }, txt_pl => { de => 'Zoll', en => 'inches', }, tmpl => '%value%%symbol%', }, ft => { ref_base => 0, symbol => '′', suffix => 'ft', txt => { de => 'Fuss', en => 'foot', }, txt_pl => { de => 'Fuss', en => 'feet', }, tmpl => '%value%%symbol%', }, yd => { ref_base => 0, suffix => 'yd', txt => { de => 'Yard', en => 'yard', }, txt_pl => { de => 'Yards', en => 'yards', }, }, mi => { ref_base => 0, suffix => 'mi', txt => { de => 'Meilen', en => 'miles', }, }, # time sec => { ref_base => 2, scale_t => 's', suffix => { de => 's', en => 's', }, txt => { de => 'Sekunde', en => 'second', }, txt_pl => { de => 'Sekunden', en => 'seconds', }, }, min => { ref_base => 2, scale_t => 'min', suffix => { de => 'Min', en => 'min', }, txt => { de => 'Minute', en => 'minute', }, txt_pl => { de => 'Minuten', en => 'minutes', }, }, hr => { ref_base => 2, scale_t => 'h', suffix => 'h', txt => { de => 'Stunde', en => 'hour', }, txt_pl => { de => 'Stunden', en => 'hours', }, }, d => { ref_base => 2, scale_t => 'd', suffix => { de => 'T', en => 'd', }, txt => { de => 'Tag', en => 'day', }, txt_pl => { de => 'Tage', en => 'days', }, }, w => { ref_base => 2, scale_t => 'w', suffix => { de => 'W', en => 'w', }, txt => { de => 'Woche', en => 'week', }, txt_pl => { de => 'Wochen', en => 'weeks', }, }, mon => { ref_base => 2, scale_t => 'm', suffix => { de => 'M', en => 'm', }, txt => { de => 'Monat', en => 'month', }, txt_pl => { de => 'Monate', en => 'Monat', }, }, y => { ref_base => 2, scale_t => 'a', suffix => { de => 'J', en => 'y', }, txt => { de => 'Jahr', en => 'year', }, txt_pl => { de => 'Jahre', en => 'years', }, }, epoch => { ref_base => 2, scale_t => 's', scope => { minValue => 0 }, rtype_description => { de => 'Unix Epoche in s seit 1970-01-01T00:00:00Z', en => 'Unix epoch in s since 1970-01-01T00:00:00Z', }, }, time => { ref_base => 2, scope => '^(([0-1]?[0-9]|[0-2]?[0-3]):([0-5]?[0-9]))$', rtype_description => { de => 'Uhrzeit hh:mm', en => 'time hh:mm', }, tmpl_long => { de => '%value%' . chr(0x00A0) . 'Uhr', en => '%value%', } }, datetime => { ref_base => 2, scope => '^(([1-2][0-9]{3})-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|30|31) (0?[1-9]|1[0-9]|2[0-3]):(0?[1-9]|[1-5][0-9]))$', rtype_description => { de => 'Datum+Uhrzeit YYYY-mm-dd hh:mm', en => 'date+time YYYY-mm-dd hh:mm', }, }, timesec => { ref_base => 900, scope => '^(([0-1]?[0-9]|[0-2]?[0-3]):([0-5]?[0-9]):([0-5]?[0-9]))$', rtype_description => { de => 'Uhrzeit hh:mm:ss', en => 'time hh:mm:ss', }, }, datetimesec => { ref_base => 2, scope => '^(([1-2][0-9]{3})-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|30|31) (0?[1-9]|1[0-9]|2[0-3]):(0?[1-9]|[1-5][0-9]):(0?[1-9]|[1-5][0-9]))$', rtype_description => { de => 'Datum+Uhrzeit YYYY-mm-dd hh:mm:ss', en => 'date+time YYYY-mm-dd hh:mm:ss', }, }, # speed bft => { ref_base => 15, suffix => 'bft', txt => { de => 'Windstärke', en => 'wind force', }, tmpl_long => '%txt% %value%', }, kn => { ref_base => 15, suffix => 'kn', txt => { de => 'Knoten', en => 'knots', }, }, fts => { ref_base => 15, ref => 'ft', ref_t => 'sec', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_t%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_t%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_t%', }, }, mph => { ref_base => 15, ref => 'mi', ref_t => 'hr', tmpl => '%value%' . chr(0x00A0) . 'mph', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_t%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_t%', }, }, kmph => { ref_base => 15, ref => 'm', ref_t => 'hr', scale_m => '1.0e3', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_t%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_t%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_t%', }, }, mps => { ref_base => 15, ref => 'm', ref_t => 'sec', scale_m => '1.0e0', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_t%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_t%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_t%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_t%', }, }, # weight mol => { ref_base => 5, suffix => 'mol', }, pg => { ref => 'g', scale_m => "1.0e-12", }, ng => { ref => 'g', scale_m => "1.0e-9", }, ug => { ref => 'g', scale_m => "1.0e-6", }, mg => { ref => 'g', scale_m => "1.0e-3", }, cg => { ref => 'g', scale_m => "1.0e-2", }, dg => { ref => 'g', scale_m => "1.0e-1", }, g => { ref_base => 1, scale_m => "1.0e0", suffix => 'g', txt => { de => 'Gramm', en => 'gram', }, }, kg => { ref => 'g', scale_m => "1.0e3", }, t => { ref => 'g', scale_m => "1.0e6", suffix => 't', txt => { de => 'Tonne', en => 'ton', }, txt_pl => { de => 'Tonnen', en => 'tons', }, }, lb => { ref_base => 1, suffix => 'lb', txt => { de => 'Pfund', en => 'pound', }, }, lbs => { ref_base => 1, suffix => 'lbs', txt => { de => 'Pfund', en => 'pound', }, }, # luminous intensity cd => { ref_base => 6, suffix => 'cd', txt => { de => 'Candela', en => 'Candela', }, }, # illumination intensity lx => { ref_base => 16, suffix => 'lx', txt => { de => 'Lux', en => 'Lux', }, }, # luminous flux lm => { ref_base => 17, suffix => 'lm', txt => { de => 'Lumen', en => 'Lumen', }, }, uvi => { ref_base => 900, suffix => 'UVI', txt => { de => 'UV-Index', en => 'UV-Index', }, tmpl => '%suffix%' . chr(0x00A0) . '%value%', tmpl_long => '%txt%' . chr(0x00A0) . '%value%', format => '%i', }, # surface area cm2 => { ref_base => 23, ref => 'm', scale_m => '1.0e-2', scale_sq => 1, }, m2 => { ref_base => 23, ref => 'm', scale_m => '1.0e0', scale_sq => 1, }, # volume cm3 => { ref_base => 18, ref => 'm', scale_m => '1.0e-2', scale_cu => 1, }, m3 => { ref_base => 18, ref => 'm', scale_m => '1.0e0', scale_cu => 1, }, ml => { ref => 'l', scale_m => '1.0e-3', }, l => { ref_base => 18, suffix => 'l', txt => { de => 'Liter', en => 'liter', }, txt_pl => { de => 'Liter', en => 'liters', }, }, hl => { ref => 'l', scale_m => '1.0e2', }, # logarithmic scale b => { ref_base => 19, scale_m => '1.0e0', suffix => 'B', txt => { de => 'Bel', en => 'Bel', }, }, db => { ref => 'b', scale_m => '1.0e-1', }, # electric current ua => { ref => 'a', scale_m => '1.0e-6', }, ma => { ref => 'a', scale_m => '1.0e-3', }, a => { ref_base => 3, scale_m => '1.0e0', suffix => 'A', txt => { de => 'Ampere', en => 'Ampere', }, }, uv => { ref => 'v', scale_m => '1.0e-6', }, mv => { ref => 'v', scale_m => '1.0e-3', }, v => { ref_base => 13, scale_m => '1.0e0', suffix => 'V', txt => { de => 'Volt', en => 'Volt', }, }, uj => { ref => 'j', scale_m => '1.0e-6', }, mj => { ref => 'j', scale_m => '1.0e-3', }, j => { ref_base => 7, scale_m => '1.0e0', suffix => 'J', txt => { de => 'Joule', en => 'Joule', }, }, uw => { ref => 'w', scale_m => '1.0e-6', }, mw => { ref => 'w', scale_m => '1.0e-3', }, w => { ref_base => 9, scale_m => '1.0e0', suffix => 'W', txt => { de => 'Watt', en => 'Watt', }, }, va => { ref => 'w', }, kw => { ref => 'w', scale_m => '1.0e3', }, megaw => { ref => 'w', scale_m => '1.0e6', }, whr => { base_ref => 7, ref => 'w', scale_m => '1.0e0', ref_t => 'hr', scale_t => 'h', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt%', en => '%value%' . chr(0x00A0) . '%txt%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl%', en => '%value%' . chr(0x00A0) . '%txt_pl%', }, rtype_description => { de => 'Wattstunde', en => 'Watt hour', } }, kwhr => { base_ref => 7, ref => 'w', scale_m => '1.0e3', ref_t => 'hr', scale_t => 'h', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt%', en => '%value%' . chr(0x00A0) . '%txt%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl%', en => '%value%' . chr(0x00A0) . '%txt_pl%', }, rtype_description => { de => 'Kilowattstunde', en => 'Kilowatt hour', } }, mwhr => { base_ref => 7, ref => 'w', scale_m => '1.0e6', ref_t => 'hr', scale_t => 'h', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%%suffix_t%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt%%txt_t%', en => '%value%' . chr(0x00A0) . '%txt% %txt_t%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt%%txt_pl%', en => '%value%' . chr(0x00A0) . '%txt% %txt_pl%', }, rtype_description => { de => 'Megawattstunde', en => 'Megawatt hour', } }, gwhr => { base_ref => 7, ref => 'w', scale_m => '1.0e9', ref_t => 'hr', scale_t => 'h', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%%suffix_t%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt%%txt_t%', en => '%value%' . chr(0x00A0) . '%txt% %txt_t%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt%%txt_pl%', en => '%value%' . chr(0x00A0) . '%txt% %txt_pl%', }, rtype_description => { de => 'Gigawattstunde', en => 'Gigawatt hour', } }, uwpscm => { ref => 'w', scale_m => '1.0e-6', ref_sq => 'm', scale_sq => '1.0e-2', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_sq%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_sq%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_sq%', }, }, uwpsm => { ref => 'w', scale_m => '1.0e-6', ref_sq => 'm', scale_sq => '1.0e0', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_sq%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_sq%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_sq%', }, }, mwpscm => { ref => 'w', scale_m => '1.0e-3', ref_sq => 'm', scale_sq => '1.0e-2', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_sq%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_sq%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_sq%', }, }, mwpsm => { ref => 'w', scale_m => '1.0e-3', ref_sq => 'm', scale_sq => '1.0e0', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_sq%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_sq%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_sq%', }, }, wpscm => { ref => 'w', scale_m => '1.0e0', ref_sq => 'm', scale_sq => '1.0e-2', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_sq%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_sq%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_sq%', }, }, wpsm => { ref => 'w', scale_m => '1.0e0', ref_sq => 'm', scale_sq => '1.0e0', format => '%.0f', tmpl => '%value%' . chr(0x00A0) . '%suffix%/%suffix_sq%', tmpl_long => { de => '%value%' . chr(0x00A0) . '%txt% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt% per %txt_sq%', }, tmpl_long_pl => { de => '%value%' . chr(0x00A0) . '%txt_pl% pro %txt_sq%', en => '%value%' . chr(0x00A0) . '%txt_pl% per %txt_sq%', }, }, coul => { ref_base => 20, suffix => 'C', txt => { de => 'Coulomb', en => 'Coulomb', }, }, far => { ref_base => 21, suffix => 'F', txt => { de => 'Farad', en => 'Farad', }, }, ohm => { ref_base => 22, symbol => 'Ω', suffix => 'Ohm', txt => { de => 'Ohm', en => 'Ohm', }, }, }; # helps the user to assign rtypes to existing readings of modules # w/o built-in rtype support # # layer 1 = module name (exception: global is valid for all modules but # with lower preference) # layer 2 = reading name as used by that module # layer 3 = template for this reading to be copied to device attribute readingsDesc. # aliasname makes a reference to another reading name to avoid duplicates # and take the opportunity to harmonise reading names here. my $readingsDB = { global => { airpress => { aliasname => 'pressure_hpa', # alias only }, azimuth => { rtype => 'gon', }, brightness => { rtype => 'lm', }, compasspoint => { rtype => 'compasspoint' }, daylight => { rtype => 'yesno', }, dewpoint => { aliasname => 'dewpoint_c', # alias only }, dewpoint_c => { rtype => 'c', }, elevation => { rtype => 'gon', }, feelslike => { aliasname => 'feelslike_c', # alias only }, feelslike_c => { rtype => 'c', }, humidity => { rtype => 'pct', formula_symbol => 'H', }, humidityabs => { aliasname => 'humidityabs_c', # alias only }, humidityabs_c => { rtype => 'c', formula_symbol => 'H', }, humidityabs_f => { rtype => 'f', formula_symbol => 'H', }, humidityabs_k => { rtype => 'k', formula_symbol => 'H', }, horizon => { rtype => 'gon', }, indoordewpoint => { aliasname => 'indoordewpoint_c', # alias only }, indoordewpoint_c => { rtype => 'c', }, indoorhumidity => { rtype => 'pct', formula_symbol => 'H', }, indoorhumidityabs => { aliasname => 'indoorhumidityabs_c', # alias only }, indoorhumidityabs_c => { rtype => 'c', }, indoortemperature => { aliasname => 'indoortemperature_c', # alias only }, indoortemperature_c => { rtype => 'c', }, israining => { rtype => 'yesno', }, level => { rtype => 'pct', }, luminosity => { rtype => 'lx', }, luminance => { rtype => 'lm', }, motion => { rtype => 'yesno', }, pct => { rtype => 'pct', }, presence => { rtype => 'reachable', }, pressure => { aliasname => 'pressure_hpa', # alias only }, pressure_hpa => { rtype => 'hpamb', }, pressure_in => { rtype => 'inhg', }, pressure_mm => { rtype => 'mmhg', }, pressureabs => { aliasname => 'pressureabs_hpamb', # alias only }, pressureabs_hpamb => { rtype => 'hpamb', }, pressureabs_in => { rtype => 'inhg', }, pressureabs_mm => { rtype => 'mmhg', }, pressureabs_psi => { aliasname => 'pressureabs_psia', }, pressureabs_psia => { rtype => 'psia', }, rain => { aliasname => 'rain_mm', # alias only }, rain_mm => { rtype => 'mm', }, rain_day => { aliasname => 'rain_day_mm', # alias only }, rain_day_mm => { rtype => 'mm', }, rain_night => { aliasname => 'rain_night_mm', # alias only }, rain_night_mm => { rtype => 'mm', }, rain_week => { aliasname => 'rain_week_mm', # alias only }, rain_week_mm => { rtype => 'mm', }, rain_month => { aliasname => 'rain_month_mm', # alias only }, rain_month_mm => { rtype => 'mm', }, rain_year => { aliasname => 'rain_year_mm', # alias only }, rain_year_mm => { rtype => 'mm', }, reachable => { rtype => 'yesno', }, snow => { aliasname => 'snow_cm', # alias only }, snow_cm => { rtype => 'cm', }, snow_day => { aliasname => 'snow_day_cm', # alias only }, snow_day_cm => { rtype => 'cm', }, snow_night => { aliasname => 'snow_night_cm', # alias only }, snow_night_cm => { rtype => 'cm', }, sunshine => { aliasname => 'solarradiation', # alias only }, solarradiation => { rtype => 'wpsm', }, temp => { aliasname => 'temperature_c', # alias only }, temp_c => { aliasname => 'temperature_c', # alias only }, temperature => { aliasname => 'temperature_c', # alias only }, temperature_c => { rtype => 'c', }, uv => { aliasname => 'uvi', # alias only }, uvi => { rtype => 'uvi', }, uvr => { rtype => 'uwpscm', }, valvedesired => { aliasname => 'valve', # alias only }, valvepos => { aliasname => 'valve', # alias only }, valveposition => { aliasname => 'valve', # alias only }, valvepostc => { aliasname => 'valve', # alias only }, valve => { rtype => 'pct', }, visibility => { aliasname => 'visibility_km', # alias only }, visibility_km => { rtype => 'km', }, wind_chill => { aliasname => 'wind_chill_c', # alias only }, wind_chill_c => { rtype => 'c', }, wind_compasspoint => { rtype => 'compasspoint' }, windspeeddirection => { aliasname => 'wind_compasspoint', # alias only }, winddirectiontext => { aliasname => 'wind_compasspoint', # alias only }, wind_direction => { aliasname => 'wind_compasspoint', # alias only }, wind_dir => { aliasname => 'wind_compasspoint', # alias only }, winddir => { aliasname => 'wind_compasspoint', # alias only }, winddirection => { aliasname => 'wind_compasspoint', # alias only }, wind_gust => { aliasname => 'wind_gust_kmh', # alias only }, wind_gust_kmh => { rtype => 'kmh', }, wind_speed => { aliasname => 'wind_speed_kmh', # alias only }, wind_speed_kmh => { rtype => 'kmh', }, } }; # Find rtype through reading name sub rname2rtype ($$@) { my ( $name, $reading, $lang ) = @_; my $details; my $r = $reading; my $l = ( $lang ? lc($lang) : "en" ); my $rt; my $guess; my %return; # reading name exactly matches rtype return $reading if ( $rtypes->{$reading} && !wantarray ); # remove some prefix or other values to # flatten reading name $r =~ s/^h?fc\d+_//i; $r =~ s/_(min|max|avg|sum|cum|min\d+m|max\d+m|avg\d+m|sum\d+m|cum\d+m)_/_/i; $r =~ s/^(min|max|avg|sum|cum|min\d+m|max\d+m|avg\d+m|sum\d+m|cum\d+m)_//i; $r =~ s/_(min|max|avg|sum|cum|min\d+m|max\d+m|avg\d+m|sum\d+m|cum\d+m)$//i; $r =~ s/.*[-_](temp)$/$1/i; # rename capital letter containing readings unless ( defined( $readingsDB->{global}{ lc($r) } ) ) { $r =~ s/^([A-Z])(.*)/\l$1$2/; $r =~ s/([A-Z][a-z0-9]+)[\/\|\-_]?/_$1/g; } $r = lc($r); if ( defined( $readingsDB->{global}{$r} ) ) { # known aliasname reading names if ( $readingsDB->{global}{$r}{aliasname} ) { my $dr = $readingsDB->{global}{$r}{aliasname}; $return{aliasname} = $dr; $rt = ( $readingsDB->{global}{$dr}{rtype} ? $readingsDB->{global}{$dr}{rtype} : undef ); } # known standard reading names else { $return{aliasname} = $reading; $rt = ( $readingsDB->{global}{$r}{rtype} ? $readingsDB->{global}{$r}{rtype} : undef ); } } # just guessing the rtype from reading name format elsif ( $r =~ /^.*_([A-Za-z0-9]+)$/i ) { $return{guess} = 1; $rt = $1; } if (wantarray) { return ( $reading, $return{aliasname}, $return{guess} ) if ( $rtypes->{$reading} ); return ( $rt, $return{aliasname}, $return{guess} ); } return $rt if ( $rt && $rtypes->{$rt} ); return undef; } # find devices based on the reading # types they offer sub rtype2dev { my %ret; foreach my $rtype (@_) { foreach my $d ( keys %defs ) { next unless ( defined( $defs{$d}{READINGS} ) && ref( $defs{$d}{READINGS} ) eq 'HASH' ); my $readingsDescr = defined( $defs{$d}{TYPE} ) && defined( $modules{ $defs{$d}{TYPE} } ) && defined( $modules{ $defs{$d}{TYPE} }{readingsDesc} ) ? $modules{ $defs{$d}{TYPE} }{readingsDesc} : undef; # if this device's module has defined readingsDesc if ( $readingsDescr && ref($readingsDescr) eq 'HASH' ) { foreach my $rn ( keys %{$readingsDescr} ) { next unless ( defined( $defs{$d}{READINGS}{$rn} ) && defined( $readingsDescr->{$rn}{rtype} ) && defined( $rtypes->{ $readingsDescr->{$rn}{rtype} } ) && $rtype eq $readingsDescr->{$rn}{rtype} ); push @{ $ret{$d} }, $rn unless ( grep ( /^$rn$/, @{ $ret{$d} } ) ); } } # try to find by commonly known reading names else { foreach my $rn ( keys %{ $defs{$d}{READINGS} } ) { my $rt = rname2rtype( $d, $rn ); push @{ $ret{$d} }, $rn if ( $rt && $rt eq $rtype && !grep ( /^$rn$/, @{ $ret{$d} } ) ); } } } } if (wantarray) { my @reta; foreach my $d ( sort { "\L$a" cmp "\L$b" } keys %ret ) { foreach my $r ( sort { "\L$a" cmp "\L$b" } @{ $ret{$d} } ) { push @reta, "$d:$r"; } } return @reta; } return \%ret; } ###################################### # package main # package main; use utf8; # Do the magic to generate value + unit combined text strings # for specified language sub replaceTemplate ($$$$;$) { my ( $device, $reading, $odesc, $lang, $value ) = @_; my $l = ( $lang ? lc($lang) : "en" ); my $txt; my $txt_long; my $r = $defs{$device}{READINGS} if ($device); return if ( !$odesc || ref($odesc) ne "HASH" ); $value = ${$odesc}{value}{$lang} if (!defined($value) && defined( $odesc->{value} ) && defined( $odesc->{value}{$lang} ) ); return $value if ( !defined($value) || $value eq "" || !defined( $odesc->{rtype} ) ); # clone my $desc; foreach ( keys %{$odesc} ) { next if ( $_ eq "scope" || $_ eq "format" || $_ eq "factor" || $_ eq "scale" ); $desc->{$_} = $odesc->{$_}; } ########## # language support # # keep only defined language if set foreach ( keys %{$desc} ) { next if (!defined( $desc->{$_} ) || ref( $desc->{$_} ) ne "HASH" ); # find any direct format if ( defined( $desc->{$_}{$l} ) ) { my $v; $v = $desc->{$_}{$l}; delete $desc->{$_}; $desc->{$_} = $v if ( defined($v) ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr $_: value replaced by language specific version ($l)" ); } # try base language format instead # and extract xx from xx_yz elsif ( $l =~ /^([a-z]+)(_([a-z]+))?$/i && defined( $desc->{$_}{$1} ) ) { my $v; $v = $desc->{$_}{$1}; delete $desc->{$_}; $desc->{$_} = $v if ( defined($v) ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr $_: value replaced by base language specific version ($1)" ); } } # if original value was a text string and we got value_num out of it, # now try to find the right standardized text value from the ARRAY my $value_num = ref( $desc->{value_num} ) eq "ARRAY" ? $desc->{value_num}[0] : ( defined( $desc->{value_num} ) ? $desc->{value_num} : undef ); if ( defined($value_num) ) { foreach ( 'suffix', 'symbol', 'txt', 'txt_pl', 'txt_long', 'txt_long_pl' ) { if ( ref( $desc->{$_} ) eq "ARRAY" && $desc->{$_}[$value_num] ) { my $v = $desc->{$_}[$value_num]; delete $desc->{$_}; $desc->{$_} = $v; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr $_: value replaced based on ARRAY found using value from 'value_num'" ); } } } ########## # template support # # add metric name to suffix and txt if ( $desc->{suffix} && $desc->{scale_txt_m} ) { Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix value rewritten by adding value from 'scale_txt_m'" ); $desc->{suffix} = $desc->{scale_txt_m} . $desc->{suffix}; } if ( $desc->{txt} && $desc->{scale_txt_long_m} ) { Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt value rewritten by adding value from 'scale_txt_long_m'" ); $desc->{txt} = $desc->{scale_txt_long_m} . lc( $desc->{txt} ); } # add time information to suffix and txt if ( $desc->{suffix} && $desc->{scale_txt_t} ) { $desc->{suffix} = $desc->{suffix} . $desc->{scale_txt_t}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix value rewritten by adding value from 'scale_txt_t'" ); } if ( $desc->{txt} && $desc->{scale_txt_long_pl_t} ) { $desc->{txt_pl} = $desc->{txt} . lc( $desc->{scale_txt_long_pl_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_pl value rewritten by adding value from 'scale_txt_long_pl_t'" ); } if ( $desc->{txt} && $desc->{scale_txt_long_t} ) { $desc->{txt} = $desc->{txt} . lc( $desc->{scale_txt_long_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt value rewritten by adding value from 'scale_txt_long_t'" ); } # add square information to suffix and txt # if no separate suffix_sq and txt_sq was found if ( !$desc->{suffix_sq} && $desc->{scale_txt_sq} ) { $desc->{suffix} = $desc->{suffix} . $desc->{scale_txt_sq}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix value rewritten by adding value from 'scale_txt_sq'" ); } if ( !$desc->{txt_sq} && $desc->{scale_txt_long_sq} ) { $desc->{txt} = $desc->{scale_txt_long_sq} . lc( $desc->{txt} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt value rewritten by adding value from 'scale_txt_long_sq'" ); } # add cubic information to suffix and txt # if no separate suffix_cu and txt_cu was found if ( !$desc->{suffix_cu} && $desc->{scale_txt_cu} ) { $desc->{suffix} = $desc->{suffix} . $desc->{scale_txt_cu}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix value rewritten by adding value from 'scale_txt_cu'" ); } if ( !$desc->{txt_cu} && $desc->{scale_txt_long_cu} ) { $desc->{txt} = $desc->{scale_txt_long_cu} . lc( $desc->{txt} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt value rewritten by adding value from 'scale_txt_long_cu'" ); } # add time information to suffix and txt # if no separate suffix_t and txt_t was found if ( !$desc->{suffix_t} && $desc->{scale_txt_t} ) { $desc->{suffix} = $desc->{suffix} . $desc->{scale_txt_t}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix value rewritten by adding value from 'scale_txt_t'" ); } if ( !$desc->{txt_t} && $desc->{scale_txt_long_pl_t} ) { $desc->{txt_pl} = $desc->{txt} . lc( $desc->{scale_txt_long_pl_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_pl value rewritten by adding value from 'scale_txt_long_pl_t'" ); } if ( !$desc->{txt_t} && $desc->{scale_txt_long_t} ) { $desc->{txt} = $desc->{txt} . lc( $desc->{scale_txt_long_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt value rewritten by adding value from 'scale_txt_long_t'" ); } # add metric name to suffix_sq if ( $desc->{suffix_sq} && $desc->{scale_txt_m_sq} && $desc->{suffix_sq} !~ /$desc->{scale_txt_m_sq}/ ) { $desc->{suffix_sq} = $desc->{scale_txt_m_sq} . $desc->{suffix_sq}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix_sq value rewritten by adding value from 'scale_txt_m_sq'" ); } if ( $desc->{txt_sq} && $desc->{scale_txt_long_m_sq} ) { $desc->{txt_sq} = $desc->{scale_txt_long_m_sq} . lc( $desc->{txt_sq} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_sq value rewritten by adding value from 'scale_txt_long_m_sq'" ); } # add square information to suffix_sq if ( $desc->{suffix_sq} && $desc->{scale_txt_sq} ) { $desc->{suffix_sq} = $desc->{suffix_sq} . $desc->{scale_txt_sq}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix_sq value rewritten by adding value from 'scale_txt_sq'" ); } if ( $desc->{txt_sq} && $desc->{scale_txt_long_sq} ) { $desc->{txt_sq} = $desc->{scale_txt_long_sq} . lc( $desc->{txt_sq} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_sq value rewritten by adding value from 'scale_txt_long_sq'" ); } # add time information to suffix_sq if ( $desc->{suffix_sq} && $desc->{scale_txt_t} ) { $desc->{suffix_sq} = $desc->{suffix_sq} . $desc->{scale_txt_t}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix_sq value rewritten by adding value from 'scale_txt_t'" ); } if ( $desc->{txt_sq} && $desc->{scale_txt_long_pl_t} ) { $desc->{txt_pl_sq} = $desc->{txt_sq} . lc( $desc->{scale_txt_long_pl_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_pl_sq value rewritten by adding value from 'scale_txt_long_pl_t'" ); } if ( $desc->{txt_sq} && $desc->{scale_txt_long_t} ) { $desc->{txt_sq} = $desc->{txt_sq} . lc( $desc->{scale_txt_long_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_sq value rewritten by adding value from 'scale_txt_long_t'" ); } # add metric name to suffix_cu if ( $desc->{suffix_cu} && $desc->{scale_txt_m_cu} ) { $desc->{suffix_cu} = $desc->{scale_txt_m_cu} . $desc->{suffix_cu}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix_cu value rewritten by adding value from 'scale_txt_m_cu'" ); } if ( $desc->{txt_cu} && $desc->{scale_txt_long_m_cu} ) { $desc->{txt_cu} = $desc->{scale_txt_long_m_cu} . lc( $desc->{txt_cu} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_cu value rewritten by adding value from 'scale_txt_long_m_cu'" ); } # add cubic information to suffix_cu if ( $desc->{suffix_cu} && $desc->{scale_txt_cu} ) { $desc->{suffix_cu} = $desc->{suffix_cu} . $desc->{scale_txt_cu}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix_cu value rewritten by adding value from 'scale_txt_cu'" ); } if ( $desc->{txt_cu} && $desc->{scale_txt_long_cu} ) { $desc->{txt_cu} = $desc->{scale_txt_long_cu} . lc( $desc->{txt_cu} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_cu value rewritten by adding value from 'scale_txt_long_cu'" ); } # add time information to suffix_cu if ( $desc->{suffix_cu} && $desc->{scale_txt_t} ) { $desc->{suffix_cu} = $desc->{suffix_cu} . $desc->{scale_txt_t}; Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr suffix_cu value rewritten by adding value from 'scale_txt_t'" ); } if ( $desc->{txt_cu} && $desc->{scale_txt_long_pl_t} ) { $desc->{txt_pl_cu} = $desc->{txt_cu} . lc( $desc->{scale_txt_long_pl_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_pl_cu value rewritten by adding value from 'scale_txt_long_pl_t'" ); } if ( $desc->{txt_cu} && $desc->{scale_txt_long_t} ) { $desc->{txt_cu} = $desc->{txt_cu} . lc( $desc->{scale_txt_long_t} ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: RAttr txt_cu value rewritten by adding value from 'scale_txt_long_t'" ); } ############################### # generate short text string # # find text template $txt = '%value%' . chr(0x00A0) . '%suffix%' if ( !$desc->{symbol} ); $txt = '%value%' . chr(0x202F) . '%symbol%' if ( $desc->{symbol} ); $txt = '%value%' . chr(0x202F) . '%symbol%' if ( $desc->{symbol} ); $txt = $desc->{tmpl} if ( $desc->{tmpl} ); $txt = '%txt%' if ( defined( $desc->{txt} ) && !looks_like_number($value) ); Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: detected txt template: $txt" ); # Replace all %text% placeholders with text value found # in old style READINGS. # Normally only TIME & VAL would be available but some modules might have # extended this and we want people to be able to make use of it. if ( $r && $reading && $r->{$reading} ) { foreach my $k ( keys %{ $r->{$reading} } ) { next if ( ref( $r->{$reading}{$k} ) ); $txt =~ s/%$k%/$r->{$reading}{$k}/g; Unit_Log3( $device, $reading, $odesc, 10, "replaceTemplate $device $reading: RAttr txt: replacing '%$k%'" ); } } # Replace all %text% placeholders with text value found # in matching reading desc hash keys foreach my $k ( keys %{$desc} ) { my $vdm = $desc->{$k}; next if ( ref($vdm) ); $vdm = UConv::decimal_mark( $vdm ) if ( defined( $desc->{decimal_mark} ) ); $txt =~ s/%$k%/$vdm/g; Unit_Log3( $device, $reading, $odesc, 10, "replaceTemplate $device $reading: RAttr txt: replacing '%$k%'" ); } return ($txt) unless (wantarray); ############################### # generate long text string in plural # # find text template if ( looks_like_number($value) && ( $value eq "0" || $value > 1 || $value < -1 ) && $desc->{txt_long_pl} ) { $txt_long = '%value%' . chr(0x00A0) . '%txt_long_pl%'; $txt_long = $desc->{tmpl_long_pl} if ( $desc->{tmpl_long_pl} ); } elsif (looks_like_number($value) && ( $value eq "0" || $value > 1 || $value < -1 ) && $desc->{txt_pl} ) { $txt_long = '%value%' . chr(0x00A0) . '%txt_pl%'; $txt_long = $desc->{tmpl_long_pl} if ( $desc->{tmpl_long_pl} ); } ############################### # generate long text string in singular # # find text template elsif ( $desc->{txt_long} ) { $txt_long = '%value%' . chr(0x00A0) . '%txt_long%'; $txt_long = $desc->{tmpl_long} if ( $desc->{tmpl_long} ); } elsif ( $desc->{txt} ) { $txt_long = '%value%' . chr(0x00A0) . '%txt%'; $txt_long = $desc->{tmpl_long} if ( $desc->{tmpl_long} ); } # for either plural or singular long text string if ($txt_long) { Unit_Log3( $device, $reading, $odesc, 9, "replaceTemplate $device $reading: detected txt_long template: $txt_long" ); # Replace all %text% placeholders with text value found # in old style READINGS. # Normally only TIME & VAL would be available but some modules might have # extended this and we want people to be able to make use of it. if ( $r && $reading && $r->{$reading} ) { foreach my $k ( keys %{ $r->{$reading} } ) { next if ( ref( $r->{$reading}{$k} ) ); $txt_long =~ s/%$k%/$r->{$reading}{$k}/g; Unit_Log3( $device, $reading, $odesc, 10, "replaceTemplate $device $reading: RAttr txt_long: replacing '%$k%'" ); } } # Replace all %text% placeholders with text value found # in matching reading desc hash keys foreach my $k ( keys %{$desc} ) { my $vdm = $desc->{$k}; next if ( ref($vdm) ); $vdm = UConv::decimal_mark( $vdm ) if ( defined( $desc->{decimal_mark} ) ); $txt_long =~ s/%$k%/$vdm/g; Unit_Log3( $device, $reading, $odesc, 10, "replaceTemplate $device $reading: RAttr txt_long: replacing '%$k%'" ); } } my $unit = $desc->{symbol} ? $desc->{symbol} : ( $desc->{suffix} ? $desc->{suffix} : ( $desc->{txt} ? $desc->{txt} : undef ) ); $txt = Encode::encode_utf8($txt) if ( defined($txt) ); $txt_long = Encode::encode_utf8($txt_long) if ( defined($txt_long) ); $unit = Encode::encode_utf8($unit) if ( defined($unit) ); return ( $txt, $txt_long, $unit ); } sub Unit_verifyValueNumber($$) { my ( $value, $scope ) = @_; my $log; my $value_num; my $verified = 0; foreach ( keys %{$scope} ) { next if ( $_ !~ /^(regex|eq|minValue|maxValue|lt|gt|le|ge)$/ ); $verified = 1; if ( !looks_like_number($value) ) { if ( $_ eq 'regex' ) { $log .= " '$value' does not match regex $scope->{$_}." if ( $value !~ /$scope->{$_}/ ); $value_num = $scope->{value_num} if ( defined( $scope->{value_num} ) ); } elsif ( $_ eq 'eq' ) { if ( $value ne $scope->{eq} ) { $log .= " $value is not equal to $scope->{eq}."; $value = $scope->{eq} if ( !$scope->{keep} || $scope->{strict} ); } $value_num = $scope->{value_num} if ( defined( $scope->{value_num} ) ); } elsif ( !$scope->{regex} && !defined( $scope->{eq} ) ) { $log .= " '$value' is not a number." if ( !$scope->{empty} && !$scope->{empty_replace} ); $value = "0" if ( !$scope->{keep} && !$scope->{empty_replace} ); $value = $scope->{empty_replace} if ( defined( $scope->{empty_replace} ) ); last; } } elsif ( $_ eq 'minValue' ) { if ( $value < $scope->{$_} ) { $log .= " $value is lesser than $scope->{$_}."; $value = $scope->{$_} if ( !$scope->{keep} || $scope->{strict} ); } } elsif ( $_ eq 'maxValue' ) { if ( $value > $scope->{$_} ) { $log .= " $value is greater than $scope->{$_}."; $value = $scope->{$_} if ( !$scope->{keep} || $scope->{strict} ); } } elsif ( $_ eq 'lt' ) { if ( $value >= $scope->{$_} ) { $log .= " $value is not $_ $scope->{$_}."; $value = $scope->{$_} if ( !$scope->{keep} || $scope->{strict} ); } } elsif ( $_ eq 'le' ) { if ( $value > $scope->{$_} ) { $log .= " $value is not $_ $scope->{$_}."; $value = $scope->{$_} if ( !$scope->{keep} || $scope->{strict} ); } } elsif ( $_ eq 'gt' ) { if ( $value <= $scope->{$_} ) { $log .= " $value is not $_ $scope->{$_}."; $value = $scope->{$_} if ( !$scope->{keep} || $scope->{strict} ); } } elsif ( $_ eq 'ge' ) { if ( $value < $scope->{$_} ) { $log .= " $value is not $_ $scope->{$_}."; $value = $scope->{$_} if ( !$scope->{keep} || $scope->{strict} ); } } elsif ( $_ eq 'eq' ) { if ( $value ne $scope->{$_} ) { $log .= " $value is not equal to $scope->{$_}."; $value = $scope->{$_} if ( !$scope->{keep} || $scope->{strict} ); } else { $log = undef; last; } } } return ( $verified, $value, $value_num, $log ); } # format a number according to desc and optional format. sub formatValue($$$;$$$$) { my ( $device, $reading, $value, $desc, $format, $scope, $lang ) = @_; $lang = "en" if ( !$lang ); my $lang_base = $lang; $lang_base =~ s/^(\w+)(_.*)$/$1/; my $value_num; if ( !defined($value) || ref($value) || AttrVal( $device, "showUnits", 1 ) eq "0" ) { Unit_Log3( $device, $reading, $desc, 10, "formatValue $device $reading: explicit return of original value" ); return $value; } $desc = readingsDesc( $device, $reading ) if ( !$desc || !ref($desc) ); if ( !$format && ( !$desc || ref($desc) ne 'HASH' ) || keys %{$desc} < 1 ) { Unit_Log3( $device, $reading, $desc, 10, "formatValue $device $reading: " . "no readings description found, returning original value" ); return $value; } # source value language my $slang = "en"; $slang = $desc->{lang} if ( $desc && $desc->{lang} ); my $slang_base = $slang; $slang_base =~ s/^(\w+)(_.*)$/$1/; # factor if ( $desc && $desc->{factor} ) { if ( ref( $desc->{factor} ) || !looks_like_number( $desc->{factor} ) ) { Unit_Log3( $device, $reading, $desc, 5, "formatValue $device $reading: ERROR: " . "RAttr factor is not numeric" ); } elsif ( looks_like_number($value) ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: " . "multiply original numeric value with factor " . $desc->{factor} ); $value *= $desc->{factor}; } else { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: factor " . "multiplication failed - '$value' is not a number" ); } } $format = $desc->{format} if ( !$format && $desc && $desc->{format} ); $scope = $desc->{scope} if ( !$scope && $desc && $desc->{scope} ); # language handling for scope if ( ref($scope) eq "HASH" && defined( $scope->{$slang} ) ) { my $v; $v = $scope->{$slang}; $scope = undef; $scope = $v; Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: RAttr scope: " . "value replaced by language specific version ($slang)" ); } elsif ( ref($scope) eq "HASH" && defined( $scope->{$slang_base} ) ) { my $v; $v = $scope->{$slang_base}; $scope = undef; $scope = $v; Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: RAttr scope: " . "value replaced by language specific version ($slang_base)" ); } Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope dump:\n" . Dumper($scope) ) if ($scope); ################################ # scope: # Check for value to be in correct scope # # Use user defined subroutine if ( ref($scope) eq 'CODE' && &$scope ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "running external subroutine $scope()" ); ( $value, $value_num ) = $scope->($value); } # scope was defined as HASH elsif ( ref($scope) eq 'HASH' ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "verifying '$value' based on HASH structure" ); my ( $log, $verified ); ( $verified, $value, $value_num, $log ) = Unit_verifyValueNumber( $value, $scope ); Unit_Log3( $device, $reading, $desc, 4, "formatValue $device $reading: scope: WARNING -$log" ) if ($log); } # scope was defined as ARRAY so let's assume value # to be the index for that array to convert # this index to a text string. # 'value_num' will be defined here to help harmonising text values over # different reading names representing similar/comparable content in # some way. elsif ( ref($scope) eq 'ARRAY' ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "verifying value '$value' based on ARRAY structure" ); # value found as index within array. # assuming this scope was defined as string in regex format if ( looks_like_number($value) && defined( $scope->[$value] ) && !ref( $scope->[$value] ) && $value =~ /$scope->[$value]/gmi ) { $value_num = $value; # some language handling # to replace original value with language specific # and FHEM harmonised value # # If specified language was found if ( ref( $desc->{txt} ) eq "HASH" && ref( $desc->{txt}{$lang} ) eq "ARRAY" && defined( $desc->{txt}{$lang}[$value] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by language specific value from 'txt' ($lang)" ); $value = $desc->{txt}{$lang}[$value]; } # also try base language elsif (ref( $desc->{txt} ) eq "HASH" && ref( $desc->{txt}{$lang_base} ) eq "ARRAY" && defined( $desc->{txt}{$lang_base}[$value] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by language specific value from 'txt' ($lang_base)" ); $value = $desc->{txt}{$lang_base}[$value]; } # fallback to english elsif (ref( $desc->{txt} ) eq "HASH" && ref( $desc->{txt}{en} ) eq "ARRAY" && defined( $desc->{txt}{en}[$value] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by language specific value from 'txt' (en / default language)" ); $value = $desc->{txt}{en}[$value]; } # if there is no language defined at all elsif ( ref( $desc->{txt} ) eq "ARRAY" && defined( $desc->{txt}[$value] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by value from 'txt'" ); $value = $desc->{txt}[$value]; } # if there is no language defined at all elsif ( !ref( $desc->{txt} ) && defined( $desc->{txt} ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by plane value from 'txt'" ); $value = $desc->{txt}; } # if there is no txt definition at all # arrays are unbalanced else { Unit_Log3( $device, $reading, $desc, 7, "formatValue $device $reading: scope: ERROR - " . "unbalanced number of items in arrays" ); } } else { my $i = 0; foreach ( @{$scope} ) { #TODO if ( !ref($_) && $value =~ /$_/gmi ) { $value_num = $i; # some language handling # to replace original value with language specific # and FHEM harmonised value # # If specified language was found if ( ref( $desc->{txt} ) eq "HASH" && ref( $desc->{txt}{$lang} ) eq "ARRAY" && defined( $desc->{txt}{$lang}[$i] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by language specific value from 'txt' ($lang)" ); $value = $desc->{txt}{$lang}[$i]; } # also try base language elsif (ref( $desc->{txt} ) eq "HASH" && ref( $desc->{txt}{$lang_base} ) eq "ARRAY" && defined( $desc->{txt}{$lang_base}[$i] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by language specific value from 'txt' ($lang_base)" ); $value = $desc->{txt}{$lang_base}[$i]; } # fallback to english elsif (ref( $desc->{txt} ) eq "HASH" && ref( $desc->{txt}{en} ) eq "ARRAY" && defined( $desc->{txt}{en}[$i] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by language specific value from 'txt' (en / default language)" ); $value = $desc->{txt}{en}[$i]; } # if there is no language defined at all elsif ( ref( $desc->{txt} ) eq "ARRAY" && defined( $desc->{txt}[$i] ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by value from 'txt'" ); $value = $desc->{txt}[$i]; } # if there is no language defined at all elsif ( !ref( $desc->{txt} ) && defined( $desc->{txt} ) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: rattr value: " . "replaced by plane value from 'txt'" ); $value = $desc->{txt}; } # if there is no txt definition at all # arrays are unbalanced else { $value = $1 if ( defined($1) ); if ( !defined($1) ) { Unit_Log3( $device, $reading, $desc, 7, "formatValue device $reading: scope: ERROR - " . "missing txt value or regex output (i=$i)" ); } } last; } #TODO elsif ( ref($_) eq "HASH" ) { if ( !looks_like_number($value) && $_->{regex} ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "searching numeric value for string '$value' (i=$i)" ); if ( $value =~ /$_->{regex}/ ) { if ( defined( $_->{value_num} ) ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "static numeric value found in HASH" ); $value_num->[0] = $i; $value_num->[1] = $_->{value_num}; } else { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "assuming indirect numeric value from ARRAY index number" ); $value_num = $i; } last; } } #TODO: wenn nummer out of scope, dann muss ein maximalwert gesetzt sein else { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "verifying numeric value '$value' (i=$i)" ); my ( $verified, $tval, $tval_num, $log ) = Unit_verifyValueNumber( $value, $_ ); if ($log) { Unit_Log3( $device, $reading, $desc, 4, "formatValue $device $reading: scope: WARNING -$log" ); } elsif ( $verified && !$log ) { if ( defined($tval_num) ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "static numeric value found in HASH" ); $value_num->[0] = $i; $value_num->[1] = $tval_num; } else { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "assuming indirect numeric value from ARRAY index number" ); $value_num = $i; } $value = $tval; last; } } } $i++; } } } # scope was defined as string, let's assume it's in regex format elsif ( defined($scope) && $scope ne "" && $value =~ /$scope/gmi ) { # if regex matches and returns a $1, let's assume this is # by intention to replace something if ( defined($1) ) { Unit_Log3( $device, $reading, $desc, 9, "formatValue $device $reading: scope: " . "'$value' replaced by regex $scope with result from variable \$1" ); $value = $1; } } # scope definition present but value seems to be out of regex scope elsif ( defined($scope) && $scope ne "" ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: scope: WARNING - " . "'$value' does not match regex $scope" ); } # format # if ( $format && !looks_like_number($value) ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue $device $reading: format: ERROR - $value is not a number" ) if ( ref($scope) eq 'HASH' && !$scope->{empty} && !$scope->{empty_replace} ); } elsif ( ref($format) eq 'CODE' && &$format ) { $value = $format->($value); } elsif ( ref($format) eq 'HASH' ) { my $v = $value; foreach my $l ( sort { $b <=> $a } keys( %{$format} ) ) { next if ( ref( $format->{$l} ) ne 'HASH' || !$format->{$l}{rescale} ); if ( $v >= $l ) { my $rescale = $format->{$l}{rescale}; $value *= $rescale if ($rescale); $value = sprintf( $format->{$l}{format}, $value ) if ( $format->{$l}{format} ); last; } } } elsif ( ref($format) eq 'ARRAY' ) { Unit_Log3( $device, $reading, $desc, 8, "formatValue($device:$reading:$desc->{rtype})" . " format not implemented: ARRAY" ); } elsif ($format) { my $rescale = $desc->{rescale}; $value *= $rescale if ($rescale); $value = sprintf( $format, $value ); } $desc->{value}{$lang} = $value; $desc->{value_num} = $value_num if ( defined($value_num) ); my ( $txt, $txt_long, $unit ) = replaceTemplate( $device, $reading, $desc, $lang, $value ); $desc->{value_txt}{$lang} = $txt; $desc->{value_txt_long}{$lang} = $txt_long if ( defined($txt_long) ); delete $desc->{value_txt_long}{$lang} if (!defined($txt_long) && defined( $desc->{value_txt_long}{$lang} ) ); return ( $txt, $txt_long, $value, $value_num, $unit ) if (wantarray); return $value if ( ( defined( $desc->{showUnits} ) && $desc->{showUnits} eq "2" ) || AttrVal( $device, "showUnits", 1 ) eq "2" ); return $txt_long if ( $desc->{showLong} && !$desc->{showShort} ); return $txt; } # find desc for device:reading sub readingsDesc($;$) { my ( $device, $reading ) = @_; $device = "" unless ( defined($device) ); my $desc = getCombinedKeyValAttr( $device, "readingsDesc", $reading ); my $rtype; if ( $desc->{rtype} ) { $rtype = $desc->{rtype}; } else { $rtype = rname2rtype( $device, $reading ); $desc->{rtype} = $rtype; } if ( $rtype && defined( $rtypes->{$rtype} ) ) { # copy information from other hashes until 3rd level foreach my $k ( keys %{ $rtypes->{$rtype} } ) { if ( ref( $rtypes->{$rtype}{$k} ) eq "HASH" ) { foreach my $k2 ( keys %{ $rtypes->{$rtype}{$k} } ) { if ( ref( $rtypes->{$rtype}{$k}{$k2} ) eq "HASH" ) { foreach ( keys %{ $rtypes->{$rtype}{$k}{$k2} } ) { delete $desc->{$k}{$k2}{$_} if ( $desc->{$k}{$k2}{$_} ); $desc->{$k}{$k2}{$_} = $rtypes->{$rtype}{$k}{$k2}{$_}; } } else { delete $desc->{$k}{$k2} if ( $desc->{$k}{$k2} ); $desc->{$k}{$k2} = $rtypes->{$rtype}{$k}{$k2}; } } } else { delete $desc->{$k} if ( $desc->{$k} ); $desc->{$k} = $rtypes->{$rtype}{$k}; } } foreach ( 'ref', 'ref_t', 'ref_sq', 'ref_cu' ) { my $suffix = $_; $suffix =~ s/^[a-z]+//; if ( defined( $desc->{$_} ) ) { my $ref = $desc->{$_}; if ( !defined( $rtypes->{$ref} ) ) { Log 1, "readingsDesc($rtype) broken reference $_"; next; } foreach my $k ( keys %{ $rtypes->{$ref} } ) { next if ( $k =~ /^scale/ ) ; # exclude scales from referenced rtype if ( !defined( $desc->{$k} ) ) { $desc->{$k} = $rtypes->{$ref}{$k}; } else { $desc->{ $k . $suffix } = $rtypes->{$ref}{$k} if ( !defined( $desc->{ $k . $suffix } ) ); } } } } if ( $desc->{scale_m} ) { my $ref = $desc->{scale_m}; foreach my $k ( keys %{ $scales_m->{$ref} } ) { $desc->{$k} = $scales_m->{$ref}{$k} if ( !defined( $desc->{$k} ) ); } } if ( $desc->{scale_sq} ) { foreach my $k ( keys %{$scales_sq} ) { $desc->{$k} = $scales_sq->{$k} if ( !defined( $desc->{$k} ) ); } my $ref = $desc->{scale_sq}; foreach my $k ( keys %{ $scales_m->{$ref} } ) { $desc->{ $k . "_sq" } = $scales_m->{$ref}{$k} if ( !defined( $desc->{ $k . "_sq" } ) ); } } if ( $desc->{scale_cu} ) { foreach my $k ( keys %{$scales_cu} ) { $desc->{$k} = $scales_cu->{$k} if ( !defined( $desc->{$k} ) ); } my $ref = $desc->{scale_cu}; foreach my $k ( keys %{ $scales_m->{$ref} } ) { $desc->{ $k . "_cu" } = $scales_m->{$ref}{$k} if ( !defined( $desc->{ $k . "_cu" } ) ); } } if ( $desc->{scale_t} ) { my $ref = $desc->{scale_t}; foreach my $k ( keys %{ $scales_t->{$ref} } ) { $desc->{$k} = $scales_t->{$ref}{$k} if ( !defined( $desc->{$k} ) ); } } $desc->{ref_base} = 999 if ( !defined( $desc->{ref_base} ) ); my $ref = $desc->{ref_base}; foreach my $k ( keys %{ $rtype_base->{$ref} } ) { $desc->{$k} = $rtype_base->{$ref}{$k} if ( !defined( $desc->{$k} ) ); } } Unit_Log3( $device, $reading, $desc, 10, "readingsDesc $device $reading:\n" . Dumper($desc) ); return $desc; } # format device:reading with optional default value and optional desc and optional format sub formatReading($$;$$$$$) { my ( $device, $reading, $default, $desc, $format, $scope, $lang ) = @_; $desc = readingsDesc( $device, $reading ) if ( !$desc ); my $value = ReadingsVal( $device, $reading, undef ); $value = $default if ( !defined($value) ); return formatValue( $device, $reading, $value, $desc, $format, $scope, $lang ); } # return unit symbol for device:reading sub readingsUnit($$;$$$) { my ( $device, $reading, $long, $combined, $desc ) = @_; $desc = readingsDesc( $device, $reading ) if ( !$desc ); return ( $desc->{suffix} ? $desc->{suffix} : undef, $desc->{symbol} ? $desc->{symbol} : undef, $desc->{txt} ? $desc->{txt} : undef ) if (wantarray); my ( $txt, $txt_long, $value, $value_num ) = formatReading( $device, $reading, "", $desc ); $txt =~ s/\s*$value\s*//; $txt_long =~ s/\s*$value\s*//; return "$txt_long ($txt)" if ( $combined && defined($txt_long) && $txt_long ne "" && defined($txt) && $txt ne "" ); return $txt_long if ( $long && defined($txt_long) && $txt_long ne "" ); return $txt if ( defined($txt) && $txt ne "" ); return ''; } # return dimension symbol for device:reading sub readingsShortname($$) { my ( $device, $reading ) = @_; if ( my $desc = readingsDesc( $device, $reading ) ) { return $desc->{formula_symbol} if ( $desc->{formula_symbol} ); return $desc->{dimension} if ( $desc->{dimension} && $desc->{dimension} =~ /^[A-Z]+$/ ); return $desc->{symbol} if ( $desc->{symbol} ); } return $reading; } # format device STATE readings according to stateFormat and optional units sub makeSTATE($;$$) { my ( $device, $stateFormat, $withUnits ) = @_; $stateFormat = '' if ( !$stateFormat ); my $hash = $defs{$device}; return $stateFormat if ( !$hash ); $stateFormat = AttrVal( $device, 'stateFormat', undef ) if ( !$stateFormat ); return '' if ( !$stateFormat ); # filter own function name to avoid loops $stateFormat =~ s/{.*}//mg if ( $stateFormat =~ /makeSTATE/ ); # use all readings if stateFormat is empty $stateFormat = join( ' ', sort keys %{ $hash->{READINGS} } ) if ( $stateFormat !~ /\w+/ ); my $txt; if ( $stateFormat =~ m/^{(.*)}$/ ) { $stateFormat = eval $1; if ($@) { $stateFormat = "Error evaluating $device stateFormat: $@"; Log 1, $stateFormat; } } else { my $r = $hash->{READINGS}; my %usedShortnames; while ( $stateFormat =~ /\b([A-Za-z\d_\.-]+):?([A-Za-z\d_\.-]+)?\b/g ) { $txt .= " " if ($txt); if ( defined( $r->{$1} ) ) { my $sname = readingsShortname( $device, $1 ); $usedShortnames{$sname}++ if ( $usedShortnames{$sname} ); $usedShortnames{$sname} = 1 if ( !$usedShortnames{$sname} ); if ( $2 && $2 ne "" ) { $txt .= "$2: "; } elsif ( $usedShortnames{$sname} > 1 ) { $txt .= "$sname" . $usedShortnames{$sname} . ": "; } else { $txt .= "$sname: "; } if ($withUnits) { $txt .= formatReading( $device, $1 ); } else { $txt .= ( formatReading( $device, $1 ) )[2]; } } else { $txt .= $1; $txt .= ":$2" if ($2); } } return $txt; } return $stateFormat; } # get combined hash for settings from module, device, global and device attributes sub getCombinedKeyValAttr($;$$) { my ( $name, $attribute, $reading ) = @_; my $d = $defs{$name} if ( $name && $defs{$name} ); my $m = $modules{ $d->{TYPE} } if ( $d && $d->{TYPE} ); my $g = $defs{"global"}; # join hashes until 3rd level my $desc; if ( $m && $attribute && $m->{$attribute} && ref( $m->{$attribute} ) eq "HASH" ) { Log3( $name, 5, "getCombinedKeyValAttr $name $reading: including HASH from module X_Initialize() function" ); foreach my $k ( keys %{ $m->{$attribute} } ) { if ( ref( $m->{$attribute}{$k} ) eq "HASH" ) { foreach my $k2 ( keys %{ $m->{$attribute}{$k} } ) { if ( ref( $m->{$attribute}{$k}{$k2} ) eq "HASH" ) { foreach ( keys %{ $m->{$attribute}{$k}{$k2} } ) { delete $desc->{$k}{$k2}{$_} if ( $desc->{$k}{$k2}{$_} ); $desc->{$k}{$k2}{$_} = $m->{$attribute}{$k}{$k2}{$_}; } } else { delete $desc->{$k}{$k2} if ( $desc->{$k}{$k2} ); $desc->{$k}{$k2} = $m->{$attribute}{$k}{$k2}; } } } else { delete $desc->{$_} if ( $desc->{$k} ); $desc->{$_} = $m->{$attribute}{$k}; } } } if ( $g && $attribute && $g->{$attribute} && ref( $g->{$attribute} ) eq "HASH" ) { Log3( $name, 5, "getCombinedKeyValAttr $name $reading: including HASH from global attribute $attribute" ); foreach my $k ( keys %{ $g->{$attribute} } ) { if ( ref( $g->{$attribute}{$k} ) eq "HASH" ) { foreach my $k2 ( keys %{ $g->{$attribute}{$k} } ) { if ( ref( $g->{$attribute}{$k}{$k2} ) eq "HASH" ) { foreach ( keys %{ $g->{$attribute}{$k}{$k2} } ) { delete $desc->{$k}{$k2}{$_} if ( $desc->{$k}{$k2}{$_} ); $desc->{$k}{$k2}{$_} = $g->{$attribute}{$k}{$k2}{$_}; } } else { delete $desc->{$k}{$k2} if ( $desc->{$k}{$k2} ); $desc->{$k}{$k2} = $g->{$attribute}{$k}{$k2}; } } } else { delete $desc->{$_} if ( $desc->{$k} ); $desc->{$_} = $g->{$attribute}{$k}; } } } if ( $d && $attribute && $d->{$attribute} && ref( $d->{$attribute} ) eq "HASH" ) { Log3( $name, 5, "getCombinedKeyValAttr $name $reading: including HASH from device attribute $attribute" ); foreach my $k ( keys %{ $d->{$attribute} } ) { if ( ref( $d->{$attribute}{$k} ) eq "HASH" ) { foreach my $k2 ( keys %{ $d->{$attribute}{$k} } ) { if ( ref( $d->{$attribute}{$k}{$k2} ) eq "HASH" ) { foreach ( keys %{ $d->{$attribute}{$k}{$k2} } ) { delete $desc->{$k}{$k2}{$_} if ( $desc->{$k}{$k2}{$_} ); $desc->{$k}{$k2}{$_} = $d->{$attribute}{$k}{$k2}{$_}; } } else { delete $desc->{$k}{$k2} if ( $desc->{$k}{$k2} ); $desc->{$k}{$k2} = $d->{$attribute}{$k}{$k2}; } } } else { delete $desc->{$_} if ( $desc->{$k} ); $desc->{$_} = $d->{$attribute}{$k}; } } } return if ( keys %{$desc} < 1 || ( $reading && ( !defined( $desc->{$reading} ) || keys %{ $desc->{$reading} } < 1 ) ) ); return $desc->{$reading} if ($reading); return $desc; } # save key/value pair to device attribute sub setKeyValAttr($$$$$) { my ( $name, $attribute, $reading, $key, $value ) = @_; my $d = $defs{$name} if ( $defs{$name} ); my $ret; return if ( !$d || ( defined( $d->{$attribute} ) && defined( $d->{$attribute}{$reading} ) && defined( $d->{$attribute}{$reading}{$key} ) && $d->{$attribute}{$reading}{$key} eq $value ) ); # rtype if ( $key =~ /^rtype$/i ) { $key = lc($key); # Show all possible values if ( $value && $value eq "?" ) { return "CURRENTLY KNOWN READING TYPES\n\n" . PrintHash( $rtypes, 0 ); } # find rtype based on reading name elsif ( !defined($value) || $value eq "" ) { $value = rname2rtype( $name, $reading ); $ret = "Set auto-detected $key for device $name $reading: " . $value if ($value); } my $curr; no strict "refs"; $curr = &$attribute( $name, $reading ) if (&$attribute); use strict "refs"; return if ( !defined($value) || $value eq "" || ( defined($curr) && defined( $curr->{$key} ) && $curr->{$key} eq $value ) || ( defined( $d->{$attribute} ) && defined( $d->{$attribute}{$reading} ) && defined( $d->{$attribute}{$reading}{$key} ) && $d->{$attribute}{$reading}{$key} eq $value ) ); return "Invalid value $value for $key: Cannot be assigned to device $name $reading" if ( !defined( $rtypes->{$value} ) ); $ret = "Changed value $key='" . $d->{$attribute}{$reading}{$key} . "' for device $name $reading to: " . $value if ( defined( defined( $d->{$attribute} ) ) && defined( $d->{$attribute}{$reading} ) && defined( $d->{$attribute}{$reading}{$key} ) && $d->{$attribute}{$reading}{$key} ne $value ); } $d->{$attribute}{$reading}{$key} = $value; # write attribute $Data::Dumper::Terse = 1; $Data::Dumper::Deepcopy = 1; $Data::Dumper::Sortkeys = 1; my $txt = Dumper( $d->{$attribute} ); $Data::Dumper::Terse = 0; $Data::Dumper::Deepcopy = 0; $Data::Dumper::Sortkeys = 0; $txt =~ s/(=>\s*\{|['"],?)\s*\n\s*/$1 /gsm; CommandAttr( undef, "$name $attribute $txt" ); return $ret; } sub deleteKeyValAttr($$$;$) { my ( $name, $attribute, $reading, $key ) = @_; my $d = $defs{$name} if ( $defs{$name} ); my $rt; return if ( !$d || !defined( $d->{$attribute} ) || !defined( $d->{$attribute}{$reading} ) || ( $key && !defined( $d->{$attribute}{$reading}{$key} ) ) ); if ($key) { $rt = " $key=" . $d->{$attribute}{$reading}{$key}; delete $d->{$attribute}{$reading}{$key}; } delete $d->{$attribute}{$reading} if ( !$key || keys %{ $d->{$attribute}{$reading} } < 1 ); # delete attribute if ( keys %{ $d->{$attribute} } < 1 ) { CommandDeleteAttr( undef, "$name $attribute" ); } # write attribute else { $Data::Dumper::Terse = 1; $Data::Dumper::Sortkeys = 1; my $txt = Dumper( $d->{$attribute} ); $Data::Dumper::Terse = 0; $Data::Dumper::Sortkeys = 0; $txt =~ s/(=>\s*\{|[\'\"0-9],?)\s*\n\s*/$1 /gsm; CommandAttr( undef, "$name $attribute $txt" ); } return "Removed $reading$rt from attribute $name $attribute"; } ################################################################ # # Wrappers for commonly used core functions in device-specific modules. # ################################################################ # Generalized function for DbLog rtype support sub Unit_DbLog_split($$) { my ( $event, $name ) = @_; my ( $reading, $value, $unit ) = ""; # exclude any multi-value events if ( $event =~ /(.*: +.*: +.*)+/ ) { Log3 $name, 5, "Unit_DbLog_split $name: Ignoring multi-value event $event"; return undef; } # exclude sum/cum and avg events elsif ( $event =~ /^.*(min|max|avg|sum|cum|min\d+m|max\d+m|avg\d+m|sum\d+m|cum\d+m): +.*/ ) { Log3 $name, 5, "Unit_DbLog_split $name: Ignoring sum/avg event $event"; return undef; } # automatic text conversions through reading type elsif ( $event =~ /^(.+): +(\S+) *(.*)/ ) { $reading = $1; my ( $txt, $txt_long, $val, $val_num, $symbol ) = formatReading( $name, $reading, "" ); if ( defined($txt) && defined($reading) && defined($val) ) { $txt =~ s/[\s\u202F\u00A0]*$val[\s\u202F\u00A0]*//; $value = $val; if ( !looks_like_number($val) && defined($val_num) ) { if ( ref($val_num) eq "ARRAY" ) { $value = $val_num->[1]; } else { $value = $val_num; } } $unit = defined($symbol) ? $symbol : $txt; } } # general event handling if ( !defined($value) && $event =~ m/^(.+): +[\D]*(\d+\.?\d*)[\s\u202F\u00A0]*[\[\{\(]?[\s\u202F\u00A0]*([\w\°\%\^\/\\]*).*/ && defined($1) && defined($2) ) { $reading = $1; $value = ReadingsNum( $name, $1, $2 ); $unit = defined($3) ? $3 : ""; } if ( !defined($value) || !looks_like_number($value) ) { Unit_Log3( $name, $reading, undef, 10, "Unit_DbLog_split $name: Ignoring event $event: value $value does not look like a number" ) if ( defined($value) ); return undef; } Unit_Log3( $name, $reading, undef, 9, "Unit_DbLog_split $name: Splitting event $event > reading=$reading value=$value unit=$unit" ); return ( $reading, $value, $unit ); } ################################################ # the new Log with integrated loglevel checking for readings sub Unit_Log3($$$$$) { my ( $dev, $rname, $desc, $loglevel, $text ) = @_; $dev = $dev->{NAME} if ( defined($dev) && ref($dev) eq "HASH" ); $desc = readingsDesc( $dev, $rname ) if ( !$desc || !ref($desc) ); my $rloglevel = $loglevel; if ( $desc && ref($desc) eq "HASH" && defined( my $rlevel = $desc->{verbose} ) ) { return if ( $loglevel > $rlevel ); $rloglevel = $rloglevel - 9; } return Log3( $dev, $rloglevel, "RType: " . $text ); } ################################################################ # # User commands # ################################################################ # command: setreadingdesc my %setreadingdeschash = ( Fn => "CommandSetReadingDesc", Hlp => " [noCheck] =[|?],set reading rtype information for ", ); $cmds{setreadingdesc} = \%setreadingdeschash; sub CommandSetReadingDesc($@) { my ( $cl, $def ) = @_; my $attribute = "readingsDesc"; my $namedef = "where is a single device name, a list separated by comma (,) or a regexp. See the devspec section in the commandref.html for details.\n" . " can be a single reading name, a list separated by comma (,) or a regexp."; my ( $a, $h ) = parseParams($def); $a->[0] = ".*" if ( !$a->[0] ); $a->[1] = ".*" if ( !$a->[1] ); return "Usage: setreadingdesc [noCheck] =[|?]\n$namedef" if ( $a->[0] eq "?" || $a->[1] eq "?" || !%{$h} ); my @rets; my $last; foreach my $name ( devspec2array( $a->[0], $cl ) ) { unless ( IsDevice($name) ) { push @rets, "Please define $name first"; next; } # do not check for existing reading if ( $name eq "global" || ( defined( $a->[2] ) && $a->[2] =~ /nocheck/i ) ) { foreach ( keys %$h ) { my $ret = setKeyValAttr( $name, $attribute, $a->[1], $_, $h->{$_} ); push @rets, $ret if ( defined($ret) ); $last = 1 if ( $h->{$_} eq "?" || $h->{$_} eq "" ); } next; } # check for existing reading my $readingspec = '^' . $a->[1] . '$'; foreach my $reading ( grep { /$readingspec/ } keys %{ $defs{$name}{READINGS} } ) { foreach ( keys %$h ) { my $ret = setKeyValAttr( $name, $attribute, $reading, $_, $h->{$_} ); push @rets, $ret if ( defined($ret) ); $last = 1 if ( $h->{$_} eq "?" || $h->{$_} eq "" ); } } last if ($last); } return join( "\n", @rets ); } # command: deletereadingdesc my %deletereadingdeschash = ( Fn => "CommandDeleteReadingDesc", Hlp => " [],delete key for ", ); $cmds{deletereadingdesc} = \%deletereadingdeschash; sub CommandDeleteReadingDesc($@) { my ( $cl, $def ) = @_; my $attribute = "readingsDesc"; my $namedef = "where is a single device name, a list separated by comma (,) or a regexp. See the devspec section in the commandref.html for details.\n" . " and can be a single reading name, a list separated by comma (,) or a regexp."; my ( $a, $h ) = parseParams($def); $a->[0] = ".*" if ( !$a->[0] ); $a->[1] = ".*" if ( !$a->[1] ); $a->[2] = ".*" if ( !$a->[2] ); return "Usage: deletereadingdesc []\n$namedef" if ( $a->[0] eq "?" || $a->[1] eq "?" ); my @rets; my $last; foreach my $name ( devspec2array( $a->[0], $cl ) ) { unless ( IsDevice($name) ) { push @rets, "Please define $name first"; next; } my $readingspec = '^' . $a->[1] . '$'; foreach my $reading ( grep { /$readingspec/ } keys %{ $defs{$name}{$attribute} } ) { my $keyspec = '^' . $a->[2] . '$'; foreach my $key ( grep { /$keyspec/ } keys %{ $defs{$name}{$attribute}{$reading} } ) { my $ret = deleteKeyValAttr( $name, $attribute, $reading, $key ); push @rets, $ret if ( defined($ret) ); } } } return join( "\n", @rets ); } 1; =pod =encoding utf8 =for :application/json;q=META.json Unit.pm { "author": [ "Julian Pawlowski " ], "x_fhem_maintainer": [ "loredo" ], "x_fhem_maintainer_github": [ "jpawlowski" ], "keywords": [ "RType", "Unit" ] } =end :application/json;q=META.json =cut