79_BDKM.pm 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315
  1. # $Id: 79_BDKM.pm 12770 2016-12-14 08:39:57Z arnoaugustin $
  2. ##############################################################################
  3. #
  4. # 79_BDKM.pm
  5. #
  6. # This file is part of FHEM.
  7. #
  8. # Fhem is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU General Public License as published by
  10. # the Free Software Foundation, either version 2 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # BDKM is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU General Public License
  19. # along with FHEM. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. # Written by Arno Augustin
  22. ##############################################################################
  23. package main;
  24. use strict;
  25. use POSIX;
  26. use warnings;
  27. #use Blocking;
  28. #use HttpUtils;
  29. use Encode;
  30. use JSON;
  31. use Time::HiRes qw(gettimeofday);
  32. use Digest::MD5 qw(md5 md5_hex md5_base64);
  33. use base qw( Exporter );
  34. use MIME::Base64;
  35. use LWP::UserAgent;
  36. use Crypt::Rijndael;
  37. my @BaseDirs = qw(
  38. /
  39. /dhwCircuits
  40. /gateway
  41. /heatingCircuits
  42. /heatSources
  43. /notifications
  44. /recordings
  45. /solarCircuits
  46. /system
  47. );
  48. #@BaseDirs = qw(/system/sensors/temperatures /dhwCircuits);
  49. my %WdToNum = qw(Mo 1 Tu 2 We 3 Th 4 Fr 5 Sa 6 Su 7);
  50. my @RC300DEFAULTS =
  51. # ID:POLL EVERY x CYCLE:MINDELTA:READINGNAME
  52. # all gateway IDs are polled (gathered) once on startup
  53. #*:1:0:* poll every cycle, difference 0 => update on difference 0 (allways)
  54. #*:1::* poll every cycle, no difference set => update on change only
  55. #*:0::* poll on startup only and update reading on change only
  56. #*:1:0.5:* poll every cycle, difference set to 0.5 => update only if difference to last read is >= 0.5
  57. #*:15::* poll on startup and every 15th cylce, update reading if changed
  58. #*:::* update reading on (get/set) only if value changed
  59. #*::0:* update reading on (get/set) always
  60. #* ID only, no ":", poll every cycle, update reading allways (same as *:1:0:*)
  61. qw(/dhwCircuits/dhw1/actualTemp:1:0.2:WaterTemp
  62. /dhwCircuits/dhw1/currentSetpoint:1::WaterDesiredTemp
  63. /dhwCircuits/dhw1/operationMode:1::WaterMode
  64. /dhwCircuits/dhw1/status:0::WaterStatus
  65. /dhwCircuits/dhw1/switchPrograms/A/1-Mo:0:0:WaterProgram-1-Mo
  66. /dhwCircuits/dhw1/switchPrograms/A/2-Tu:0:0:WaterProgram-2-Tu
  67. /dhwCircuits/dhw1/switchPrograms/A/3-We:0:0:WaterProgram-3-We
  68. /dhwCircuits/dhw1/switchPrograms/A/4-Th:0:0:WaterProgram-4-Th
  69. /dhwCircuits/dhw1/switchPrograms/A/5-Fr:0:0:WaterProgram-5-Fr
  70. /dhwCircuits/dhw1/switchPrograms/A/6-Sa:0:0:WaterProgram-6-Sa
  71. /dhwCircuits/dhw1/switchPrograms/A/7-Su:0:0:WaterProgram-7-Su
  72. /dhwCircuits/dhw1/temperatureLevels/high:1::WaterDayTemp
  73. /dhwCircuits/dhw1/waterFlow:::waterFlow
  74. /dhwCircuits/dhw1/workingTime:::WaterWorkingTime
  75. /gateway/DateTime:0:0:DateTime
  76. /gateway/instAccess:0:0:InstAccess
  77. /gateway/uuid:::Uuid
  78. /gateway/versionFirmware:::FirmwareVersion
  79. /heatSources/ChimneySweeper:::ChimneySweeper
  80. /heatSources/flameCurrent:::FlameCurrent
  81. /heatSources/gasAirPressure:0:0:GasAirPressure
  82. /heatSources/hs1/energyReservoir:::EnergyReservoir
  83. /heatSources/hs1/flameStatus:::FlameStatus
  84. /heatSources/hs1/fuel/caloricValue:0:0:CaloricValue
  85. /heatSources/hs1/fuel/density:0:0:FuelDensity
  86. /heatSources/hs1/fuelConsmptCorrFactor:0:0:FuelConsmptCorrFactor
  87. /heatSources/hs1/info:::HeatSourceInfo
  88. /heatSources/hs1/nominalFuelConsumption:0:0:FuelConsumption
  89. /heatSources/hs1/reservoirAlert:0:0:ReservoirAlert
  90. /heatSources/hs1/supplyTemperatureSetpoint:0:0:SupplyTemperatureSetpoint
  91. /heatSources/hs1/type:::HeatSourceType
  92. /heatSources/info:::HeatSourceInfo
  93. /heatSources/numberOfStarts:0:0:NumberOfStarts
  94. /heatSources/systemPressure:20:0.2:SystemPressure
  95. /heatSources/workingTime/centralHeating:0:0:CentralHeatingWorkingTime
  96. /heatSources/workingTime/secondBurner:0:0:SecondBurnerWorkingTime
  97. /heatSources/workingTime/totalSystem:0:0:SystemWorkingTime
  98. /heatingCircuits/hc1/activeSwitchProgram:0:0:ActiveSwitchProgram
  99. /heatingCircuits/hc1/actualSupplyTemperature:0:0:HC1SupplyTemp
  100. /heatingCircuits/hc1/currentRoomSetpoint:1::RoomDesiredTemp
  101. /heatingCircuits/hc1/fastHeatupFactor:0:0:HeatupFactor
  102. /heatingCircuits/hc1/manualRoomSetpoint:10::RoomManualDesiredTemp
  103. /heatingCircuits/hc1/operationMode:10::HeatMode
  104. /heatingCircuits/hc1/pumpModulation:1:10:PumpModulation
  105. /heatingCircuits/hc1/status:0:0:Status
  106. /heatingCircuits/hc1/switchPrograms/A/1-Mo:0:0:ProgramA1-Mo
  107. /heatingCircuits/hc1/switchPrograms/A/2-Tu:0:0:ProgramA2-Tu
  108. /heatingCircuits/hc1/switchPrograms/A/3-We:0:0:ProgramA3-We
  109. /heatingCircuits/hc1/switchPrograms/A/4-Th:0:0:ProgramA4-Th
  110. /heatingCircuits/hc1/switchPrograms/A/5-Fr:0:0:ProgramA5-Fr
  111. /heatingCircuits/hc1/switchPrograms/A/6-Sa:0:0:ProgramA6-Sa
  112. /heatingCircuits/hc1/switchPrograms/A/7-Su:0:0:ProgramA7-Su
  113. /heatingCircuits/hc1/switchPrograms/B/1-Mo:0:0:ProgramB1-Mo
  114. /heatingCircuits/hc1/switchPrograms/B/2-Tu:0:0:ProgramB2-Tu
  115. /heatingCircuits/hc1/switchPrograms/B/3-We:0:0:ProgramB3-We
  116. /heatingCircuits/hc1/switchPrograms/B/4-Th:0:0:ProgramB4-Th
  117. /heatingCircuits/hc1/switchPrograms/B/5-Fr:0:0:ProgramB5-Fr
  118. /heatingCircuits/hc1/switchPrograms/B/6-Sa:0:0:ProgramB6-Sa
  119. /heatingCircuits/hc1/switchPrograms/B/7-Su:0:0:ProgramB7-Su
  120. /heatingCircuits/hc1/temperatureLevels/comfort2:10::ComfortTemp
  121. /heatingCircuits/hc1/temperatureLevels/eco:10::EcoTemp
  122. /heatingCircuits/hc1/temporaryRoomSetpoint:1::RoomTemporaryDesiredTemp
  123. /notifications:0:0:Notifications
  124. /system/brand:0:0:SystemBrand
  125. /system/bus:::BusType
  126. /system/healthStatus:10::Health
  127. /system/heatSources/hs1/actualModulation:1::PowerModulation
  128. /system/heatSources/hs1/actualPower:1::Power
  129. /system/holidayModes/hm1/assignedTo:0:0:Holiday1Assign
  130. /system/holidayModes/hm1/dhwMode:0:0:Holiday1WaterMode
  131. /system/holidayModes/hm1/hcMode:0:0:Holiday1HeatMode
  132. /system/holidayModes/hm1/startStop:0:0:Holiday1
  133. /system/holidayModes/hm2/assignedTo:0:0:Holiday2Assign
  134. /system/holidayModes/hm2/dhwMode:0:0:Holiday2WaterMode
  135. /system/holidayModes/hm2/hcMode:0:0:Holiday2HeatMode
  136. /system/holidayModes/hm2/startStop:0:0:Holiday2
  137. /system/holidayModes/hm3/assignedTo:0:0:Holiday3Assign
  138. /system/holidayModes/hm3/dhwMode:0:0:Holiday3WaterMode
  139. /system/holidayModes/hm3/hcMode:0:0:Holiday3HeatMode
  140. /system/holidayModes/hm3/startStop:0:0:Holiday3
  141. /system/holidayModes/hm4/assignedTo:0:0:Holiday4Assign
  142. /system/holidayModes/hm4/dhwMode:0:0:Holiday4WaterMode
  143. /system/holidayModes/hm4/hcMode:0:0:Holiday4HeatMode
  144. /system/holidayModes/hm4/startStop:0:0:Holiday4
  145. /system/holidayModes/hm5/assignedTo:0:0:Holiday5Assign
  146. /system/holidayModes/hm5/dhwMode:0:0:Holiday5WaterMode
  147. /system/holidayModes/hm5/hcMode:0:0:Holiday5HeatMode
  148. /system/holidayModes/hm5/startStop:0:0:Holiday5
  149. /system/info:::SystemInfo
  150. /system/minOutdoorTemp:0:0:MinOutdoorTemp
  151. /system/sensors/temperatures/outdoor_t1:1:0.5:OutdoorTemp
  152. /system/sensors/temperatures/return:1:0.5:ReturnTemp
  153. /system/sensors/temperatures/supply_t1:1:0.5:SupplyTemp
  154. /system/sensors/temperatures/supply_t1_setpoint:1:0.5:DesiredSupplyTemp
  155. /system/systemType:::SystemType
  156. );
  157. # I don't know anything about RC30 and RC35 - feel free to fill with knowledge:
  158. my @RC30DEFAULTS =
  159. qw(/gateway/DateTime:0:0:DateTime
  160. );
  161. my @RC35DEFAULTS =
  162. qw(/gateway/DateTime:0:0:DateTime
  163. );
  164. # extra valid value not in range (set by gateway)
  165. my %extra_value=
  166. qw(/heatingCircuits/hc1/fastHeatupFactor 0
  167. /heatingCircuits/hc1/temporaryRoomSetpoint -1
  168. /gateway/DateTime now);
  169. sub BDKM_Define($$);
  170. sub BDKM_Undefine($$);
  171. sub BDKM_Initialize($)
  172. {
  173. my ($hash) = @_;
  174. $hash->{STATE} = "Init";
  175. $hash->{DefFn} = "BDKM_Define";
  176. $hash->{UndefFn} = "BDKM_Undefine";
  177. $hash->{SetFn} = "BDKM_Set";
  178. $hash->{GetFn} = "BDKM_Get";
  179. $hash->{AttrFn} = "BDKM_Attr";
  180. $hash->{DeleteFn} = "BDKM_Undefine";
  181. $hash->{AttrList} =
  182. "BaseInterval " .
  183. "InterPollDelay " .
  184. "PollIds:textField-long " .
  185. "HttpTimeout " .
  186. $readingFnAttributes;
  187. return undef;
  188. }
  189. sub BDKM_Define($$)
  190. {
  191. my ($hash, $def) = @_;
  192. my @a = split(/\s+/, $def);
  193. my $name = $a[0];
  194. my $salt = "";
  195. my $cryptkey="";
  196. my $usage="usage: \"define <devicename> BDKM <IPv4-address|hostname> <GatewayPassword> <PrivatePassword> <md5salt>\" or\n".
  197. "\"define <devicename> BDKM <IPv4-address|hostname> <AES-Key (see:https://ssl-account.com/km200.andreashahn.info)>\"";
  198. (@a == 4 or @a ==6) or return "$name $usage";
  199. $hash->{NAME} = $name;
  200. $hash->{STATE} = "define";
  201. my $ip = $a[2];
  202. ($ip =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/ and
  203. $1<256 and $2<256 and $3<256 and $4<256) or
  204. ($ip =~ m/(?=^.{1,253}$)(^(((?!-)[a-zA-Z0-9-]{1,63}(?<!-))|((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63})$)/) or
  205. return "$name IP or hostname invalid, $usage";
  206. if(@a == 6) {
  207. my @passwd = ($a[3],$a[4]); # gateway,private passwd
  208. $salt=$a[5];
  209. if(!($passwd[0] =~ /-/)) { # must be base64
  210. my $i;
  211. foreach $i ((0,1)) {
  212. $_ = decode_base64($passwd[$i]);
  213. s/[\r\n]//g;
  214. $passwd[$i] = $_;
  215. }
  216. }
  217. $passwd[0] =~ tr/-//d;
  218. if(length($passwd[0]) != 16 or
  219. length($passwd[1]) == 0) {
  220. Log3 $name, 1, "$name please check passwords";
  221. return "$name ERROR gateway password needs format ".
  222. "\"aaaa-bbbb-cccc-dddd\"\n".
  223. "password may be encoded base64 to make it less human readable";
  224. }
  225. $salt = pack('H*',$salt);
  226. $cryptkey = md5($passwd[0].$salt).md5($salt.$passwd[1]);
  227. } elsif (@a == 4) {
  228. # complete AES-Key from define. Can be generated here:
  229. # https://ssl-account.com/km200.andreashahn.info
  230. $cryptkey = pack('H*',$a[3]);
  231. }
  232. Log3 $name, 3, "$name using AES-Key: ".unpack("H*",$cryptkey)."\n";
  233. $hash->{CRYPT} =
  234. Crypt::Rijndael->new($cryptkey, Crypt::Rijndael::MODE_ECB() );
  235. $hash->{NAME} = $name;
  236. $hash->{IP} = $ip;
  237. $hash->{SEQUENCE} = 0;
  238. $hash->{POLLIDS} = {}; # from attr PollIds
  239. $hash->{UPDATES} = []; # Ids to check for update reading after polling
  240. $hash->{REALTOUSER} = {}; # Hash to transform real IDs to readings
  241. $hash->{USERTOREAL} = {}; # Hash to readings to real IDs
  242. $hash->{IDS} = {}; # Hash containing IDS of first full poll
  243. $hash->{VERSION} = '$Id: 79_BDKM.pm 12770 2016-12-14 08:39:57Z arnoaugustin $';
  244. # init attrs to defaults:
  245. map {BDKM_Attr("del",$name,$_)} qw(BaseInterval InterPollDelay ReadBackDelay HttpTimeout);
  246. # delay start to have a chance that all attrs are set
  247. BDKM_Timer($hash,10,"BDKM_doSequence");
  248. return undef;
  249. }
  250. sub BDKM_Attr(@)
  251. {
  252. my ($cmd,$name,$attr,$val) = @_;
  253. my $hash = $defs{$name};
  254. my $error = "$name: ERROR attribute $attr ";
  255. my $del = $cmd =~ /del/;
  256. local $_;
  257. defined $val or $val="";
  258. if ($attr eq "BaseInterval") {
  259. $del and $val = 120; # default
  260. if($val !~ /^\d+$/ or $val < 30) {
  261. return $error."needs interger value >= 30";
  262. } else {
  263. $hash->{BASEINTERVAL} = $val;
  264. }
  265. } elsif ($attr eq "InterPollDelay") {
  266. $del and $val = 0; # default
  267. if($val !~ /^\d+$/) {
  268. return $error."needs interger value";
  269. } else {
  270. $hash->{INTERPOLLDELAY} = $val/1000;
  271. }
  272. } elsif($attr eq "ReadBackDelay") {
  273. $del and $val = 500;
  274. if($val !~ /^\d+$/ or $val < 100 or $val > 2000) {
  275. return $error."needs interger value (milliseconds) between 100 and 2000";
  276. } else {
  277. $hash->{READBACKDELAY} = $val;
  278. }
  279. } elsif ($attr eq "HttpTimeout") {
  280. $del and $val = 10; # default
  281. $val =~ /^([0-9]+|[0-9]+\.?[0.9]+)$/ or return $error."needs numeric value";
  282. $hash->{HTTPTIMEOUT} = $val;
  283. } elsif($attr eq "PollIds") {
  284. $hash->{POLLIDS} = {}; # no more polling
  285. $hash->{UPDATES} = []; # no updates for possibly running poll
  286. $del and return undef;
  287. $hash->{REALTOUSER} = {};
  288. $hash->{USERTOREAL} = {};
  289. my @ids=();
  290. # add defaults if set
  291. $val =~ s|RC300DEFAULTS||g and push(@ids, @RC300DEFAULTS);
  292. $val =~ s|RC35DEFAULTS||g and push(@ids, @RC35DEFAULTS);
  293. $val =~ s|RC30DEFAULTS||g and push(@ids, @RC30DEFAULTS);
  294. push(@ids,split(/\s+/s,$val));
  295. my $err = $error."needs space separated valid gateway IDs like\n".
  296. "/system/sensors/temperatures/return:2:0.5:ReturnTemp\nor just\n".
  297. "/system/sensors/temperatures/return:::\nor just\n".
  298. "/system/sensors/temperatures/return\n";
  299. foreach (@ids) {
  300. s|\s+||gs;
  301. /[A-z]/ or next;
  302. my($id,$modulo,$delta,$replace);
  303. if(m|(^/[A-z0-9\-_/]+[A-z0-9])$|) { # no ":"
  304. # poll id every cycle, no delta check (allways update), no replacement
  305. ($id,$modulo,$delta,$replace) = ($1,1,0,"");
  306. } else { # colon separated extras id:modulo:delta:replace
  307. unless(($id,$modulo,$delta,$replace) =
  308. m|(^/[A-z0-9\-_/]+[A-z0-9]):[-]*(\d*):([0-9]*\.?[0-9]*):(.*)$|) {
  309. return $err;
  310. }
  311. ($modulo eq "" or $modulo =~ '-') and $modulo = -1;
  312. }
  313. # check pathes:
  314. my $ok = 0;
  315. foreach my $dir (@BaseDirs) {
  316. $dir eq "/" and next;
  317. !index($id,$dir,0) and $ok=1 and last;
  318. }
  319. $ok or return $error."$id is not a valid gateway ID";
  320. defined $hash->{POLLIDS}{$id} and
  321. Log3 $hash, 4, "$name attr PollIds - Overwritig definition of $id";
  322. $delta eq "" or $delta += 0.0;
  323. $hash->{POLLIDS}{$id}{MODULO} =int($modulo);
  324. $hash->{POLLIDS}{$id}{DELTA} = $delta;
  325. if($replace) {
  326. $hash->{REALTOUSER}{$id}=$replace;
  327. $hash->{USERTOREAL}{$replace}=$id;
  328. } else {
  329. # remove replacements for IDs if overwritten.
  330. if(defined $hash->{REALTOUSER}{$id}) {
  331. delete $hash->{REALTOUSER}{$id};
  332. map {
  333. $hash->{USERTOREAL}{$_} eq $id and delete $hash->{USERTOREAL}{$_}
  334. } keys %{$hash->{USERTOREAL}};
  335. }
  336. }
  337. }
  338. }
  339. return undef;
  340. }
  341. sub BDKM_doSequence($)
  342. {
  343. my ($hash) = @_;
  344. # BDKM_doSequence is never called directly. It's only triggered by its own timer.
  345. # restart timer for next sequence
  346. BDKM_Timer($hash,$hash->{BASEINTERVAL},"BDKM_doSequence");
  347. # only start polling if we are not polling (e.g. due to network promlems)
  348. if($hash->{ISPOLLING}) {
  349. Log3 $hash, 3, $hash->{NAME}." ERROR: trying to start new sequence while previous not finished";
  350. Log3 $hash, 3, $hash->{NAME}." Gateway not responding? BaseInterval too short? InterPollDelay too high?";
  351. return;
  352. }
  353. $hash->{ISPOLLING}=1;
  354. my $seq = $hash->{SEQUENCE};
  355. my $h = $hash->{POLLIDS};
  356. Log3 $hash, 4, "$hash->{NAME} starting polling sequence #".$seq;
  357. if(!$seq) { # do full poll and init $hash->{IDS}
  358. @{$hash->{JOBQUEUE}} = @BaseDirs;
  359. # update only modulos >= 0
  360. @{$hash->{UPDATES}} =
  361. sort grep {$h->{$_}{MODULO} >= 0} keys(%$h);
  362. } else {
  363. $h or return; # no ids to poll
  364. # only poll known IDs which are in turn
  365. @{$hash->{UPDATES}} =
  366. sort grep {$h->{$_}{MODULO} > 0 and $seq % $h->{$_}{MODULO} == 0 and defined $hash->{IDS}{$_}} keys(%$h);
  367. # JOBQUEUE: remove special switchPrograms and transform to the real base reading:
  368. my %seen=();
  369. @{$hash->{JOBQUEUE}} = BDKM_MapSwitchPrograms($hash->{UPDATES});
  370. }
  371. Log3 $hash, 6, $hash->{NAME}." jobqueue is:".join(" ",@{$hash->{JOBQUEUE}})."\n";
  372. Log3 $hash, 6, $hash->{NAME}." elements to update after polling: ".join(" ",@{$hash->{UPDATES}})."\n";
  373. readingsSingleUpdate($hash, "state", "polling", 0);
  374. BDKM_JobQueueNextId($hash);
  375. }
  376. sub BDKM_JobQueueNextId($)
  377. {
  378. my ($hash) = @_;
  379. if(@{$hash->{JOBQUEUE}}) { # still ids to poll
  380. my $id = (@{$hash->{JOBQUEUE}})[0];
  381. Log3 $hash, 5, "$hash->{NAME} reading $id";
  382. # get next type
  383. BDKM_HttpGET($hash,$id,\&BDKM_JobQueueNextIdHttpDone);
  384. } else {
  385. BDKM_UpdateReadings($hash,$hash->{UPDATES});
  386. $hash->{SEQUENCE}++;
  387. $hash->{ISPOLLING}=0;
  388. Log3 $hash, 4, $hash->{NAME}." update ".join(" ",@{$hash->{UPDATES}})."\n";
  389. readingsSingleUpdate($hash, "state", "idle", 0);
  390. }
  391. }
  392. sub BDKM_JobQueueNextIdHttpDone($)
  393. {
  394. my ($param, $err, $data) = @_;
  395. my $hash = $param->{hash};
  396. my $name = $hash ->{NAME};
  397. my $json;
  398. if($err) {
  399. readingsSingleUpdate($hash, "state",
  400. "reading ids ERROR - retrying every 60s", 1);
  401. Log3 $name, 2, "$name communication ERROR in state $hash->{STATE}: $err";
  402. # try again in 60s
  403. BDKM_Timer($hash,60,"BDKM_JobQueueNextId");
  404. return;
  405. }
  406. my $hth= $param->{httpheader};
  407. $hth =~ s/[\r\n]//g;
  408. Log3 $name, 5, "$name HTTP done @{$hash->{JOBQUEUE}}[0],$hth";
  409. # did this type, remove from job queue:
  410. my $id = shift(@{$hash->{JOBQUEUE}});
  411. ($json,$data) = BDKM_decode_http_data($hash,$data);
  412. if($json) {
  413. if (!$hash->{SEQUENCE} and $json and $json->{type} eq "refEnum") { # init only
  414. # new type
  415. foreach my $item (@{$json->{references}}) {
  416. my $entry = $item->{id};
  417. #exists $hash->{IGNOREIDS}{$entry} and next; # ignore
  418. # push to job queue
  419. push(@{$hash->{JOBQUEUE}},$entry);
  420. }
  421. } else {
  422. BDKM_update_id_from_json($hash,$json);
  423. }
  424. } else {
  425. if(!$hash->{SEQUENCE}) {
  426. if($id ne "/") {
  427. $hash->{IDS}{$id}{RAWDATA} = 1;
  428. $hth =~ s|HTTP/...|HTTP|;
  429. $hth =~ s/\s+/_/g;
  430. $hth =~ /200/ or $hash->{IDS}{$id}{HTTPHEADER} = $hth;
  431. $hth =~ /200/ or $data = $hth;
  432. }
  433. Log3 $hash, 4, "$name $id: $data";
  434. }
  435. }
  436. if($hash->{INTERPOLLDELAY}) {
  437. BDKM_Timer($hash,$hash->{INTERPOLLDELAY},"BDKM_JobQueueNextId");
  438. } else {
  439. BDKM_JobQueueNextId($hash);
  440. }
  441. return;
  442. }
  443. sub BDKM_UpdateReadings($$)
  444. {
  445. my ($hash,$listref) = @_;
  446. readingsBeginUpdate($hash);
  447. foreach my $id (@$listref) {
  448. my $val = $hash->{IDS}{$id}{VALUE};
  449. defined $val or next;
  450. Log3 $hash, 5, "Check reading update for $id $val";
  451. my $reading = defined $hash->{REALTOUSER}{$id} ?
  452. $hash->{REALTOUSER}{$id} : $id;
  453. my $rdval = $hash->{READINGS}{$reading}{VAL};
  454. if(defined($rdval) and defined $hash->{POLLIDS}{$id}) {
  455. my $delta = $hash->{POLLIDS}{$id}{DELTA};
  456. # same as last - skip
  457. $delta eq "" and $rdval eq $val and next;
  458. # difference too small - skip
  459. $delta and abs($rdval-$val) < $delta and next;
  460. }
  461. Log3 $hash, 4, "$hash->{NAME} update reading $reading $val";
  462. readingsBulkUpdate($hash,$reading,$val);
  463. }
  464. readingsEndUpdate($hash,1);
  465. }
  466. sub BDKM_Undefine($$)
  467. {
  468. my ($hash, $def) = @_;
  469. my $name = $hash->{NAME};
  470. BDKM_RemoveTimer($hash);
  471. return undef;
  472. }
  473. sub BDKM_GetInfo
  474. {
  475. my ($hash, $matches) = @_;
  476. no warnings 'uninitialized';
  477. my $fmt="%-50.50s %-25.25s %-23.23s %s %-30.30s %-10.10s %-10.10s\n";
  478. my $header =sprintf($fmt,
  479. "Gateway ID", "FHEM Reading (Alias)", "Last Value Read", "TW",
  480. "Valid Values", "Poll", "Rd.Update");
  481. my $llll = ("-" x length($header))."\n";
  482. my @ids;
  483. if($matches) {
  484. # loop over all possible IDs and aliases and check if
  485. # they match given regexp inputs
  486. my %seen=();
  487. map {
  488. my $regex = qr/$_/;
  489. map {
  490. if($_ =~ $regex) { # match on input to INFO
  491. my $realid = defined $hash->{USERTOREAL}{$_} ?
  492. $hash->{USERTOREAL}{$_} : $_;
  493. !$seen{$realid}++ and push(@ids,$realid);
  494. }
  495. } (keys %{$hash->{IDS}}, keys %{$hash->{USERTOREAL}});
  496. } split(/\s+/,$matches);
  497. } else {
  498. # use all IDs
  499. @ids = keys %{$hash->{IDS}};
  500. }
  501. my @lines= sort map {
  502. my $id=$_;
  503. my $h=$hash->{IDS}{$id};
  504. my $p=$hash->{POLLIDS}{$id};
  505. my $m = $p->{MODULO};
  506. my $d = $p->{DELTA};
  507. my $u = $h->{UNIT};
  508. my $type = substr($h->{TYPE},0,1);
  509. my $flags = ($type ? $type : " ").($h->{WRITEABLE} ? '+' : '-');
  510. my $a = defined $h->{RAWDATA} ? $h->{HTTPHEADER} : $h->{ALLOWED};
  511. $u =~ s/µ/u/g;
  512. $a =~ s/ /,/g;
  513. sprintf($fmt,
  514. $id,
  515. $hash->{REALTOUSER}{$id},
  516. $h->{VALUE}.($u ? " $u":""),
  517. $flags,
  518. defined $a ? $a :
  519. $h->{MIN} ? "[$h->{MIN}:$h->{MAX}]" : "",
  520. (!defined $m or $m < 0) ? "" :
  521. $m == 0 ? "once" :
  522. $m == 1 ? "always" :
  523. $m >1 ? "every $m" : "",
  524. (!defined $d or $d eq "") ? "on change" :
  525. $d == 0 ? "always" : "Δ >= $d"
  526. );
  527. } @ids;
  528. my $footer= $matches ? "" :
  529. q(* The table shows all known gateway IDs. A "+" sign in the W column means the ID is writeable.
  530. Long entries may be cut due to formating.
  531. Ranges for Valid Values ranges are shown as: [from:to]
  532. When no JSON data can be fetched the HTTP error is shown.
  533. Temperatures are normaly allowed to set in 0.5 C steps only.
  534. On startup all IDs are gathered once but do not automatically generate a fhem reading.
  535. IDs which shoud generate readings not only with the set/get command need to be defined with the "PollIds" attribute.
  536. Poll:
  537. always => ID is polled every cycle (PollIds setting *:1:*:*)
  538. every X => ID is only polled every Xth cycle (PollIds setting *:X:*:*)
  539. once => After gathering process on startup this ID is checked for reading update (PollIds setting *:0:*:*)
  540. '' => update checks only on get/set command (PollIds setting *::*:* or not set)
  541. Redings Udate:
  542. always => Reading Update is always done on value update (PollIds setting *:*:0:*)
  543. Δ >= X => Reading Update is done when difference to last reading was at least X (PollIds setting *:*:X:*)
  544. on change => Reading Update is done when value has changed to last reading (PollIds setting *:*::*)
  545. );
  546. return "\n".$header.$llll.join("",@lines).$llll.$footer;
  547. }
  548. sub BDKM_Set($@)
  549. {
  550. my ( $hash, $name, $id, @values) = @_;
  551. if(!defined $hash->{IDS}{$id} and !defined $hash->{USERTOREAL}{$id}) {
  552. no warnings 'uninitialized';
  553. # only print aliased commands:
  554. my @writeable=sort grep {
  555. $hash->{IDS}{$hash->{USERTOREAL}{$_}}{WRITEABLE}
  556. } keys %{$hash->{USERTOREAL}};
  557. my @cmds=map {
  558. my @vals=();
  559. my $realid=$hash->{USERTOREAL}{$_};
  560. defined $extra_value{$realid} and push (@vals,$extra_value{$realid});
  561. my $h=$hash->{IDS}{$realid};
  562. if ($realid =~ /HeatupFactor/) {
  563. push(@vals,(10,20,30,40,50,60,70,80,90,100));
  564. } elsif(defined $h->{ALLOWED}) {
  565. $h->{TYPE} ne "arrayData" and push(@vals,split(/\s+/,$h->{ALLOWED}));
  566. } elsif (defined $h->{MAX}) {
  567. if($h->{UNIT} eq "C") {
  568. for(my $i=$h->{MIN}; $i <= $h->{MAX}; $i+=0.5) {
  569. push(@vals,$i);
  570. }
  571. }
  572. }
  573. if (@vals) {
  574. $_.=":".join(',',@vals);
  575. } else {
  576. $_;
  577. }
  578. } @writeable;
  579. return "Unknown argument $id, choose one of ".join(" ",@cmds);
  580. }
  581. @values or
  582. return "usage: set $hash->{NAME} <ID> <value ...>";
  583. my $value=join(" ",@values);
  584. my $ret;
  585. $ret = BDKM_SetId($hash, $id, $value);
  586. $ret !~ /Unable to set/ and return $ret;
  587. BDKM_msleep(2000);
  588. $ret = BDKM_SetId($hash, $id, $value);
  589. return $ret;
  590. }
  591. sub BDKM_HttpTest
  592. {
  593. my ($hash,$id,$method,$data) = @_;
  594. my $param = {
  595. url => "http://" . $hash->{IP} . $id,
  596. hash => $hash,
  597. data => $data,
  598. method => $method,
  599. header => "agent: PortalTeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json",
  600. };
  601. $param->{timeout} = 3;
  602. my @a= HttpUtils_BlockingGet($param); #returns ($err, $data)
  603. $param->{hash}=0;
  604. }
  605. sub BDKM_SetId($@)
  606. {
  607. my ($hash,$id,$value) = @_;
  608. my $name=$hash->{NAME};
  609. defined $hash->{USERTOREAL}{$id} and $id = $hash->{USERTOREAL}{$id};
  610. # set getway time to host time:
  611. $id eq "/gateway/DateTime" and $value eq "now" and
  612. $value=strftime("%Y-%m-%dT%H:%M:%S", localtime);
  613. my $data;
  614. my $err;
  615. if($value =~ /\s+test$/ or defined $hash->{IDS}{$id}{RAWDATA}) {
  616. # we dont know anything about that...yet
  617. # try raw data send
  618. $value =~ s/\s+test$//g;
  619. $id =~ /firmware/i and return; # better...if we don't know what we do.
  620. Log3 $name, 3, "$name set rawpost $id value $value";
  621. $data = BDKM_Encrypt($hash,$value);
  622. Log3 $name, 3, "$name http PUT $id encrypted data $data";
  623. my $a;
  624. ($data,$err,$a) = BDKM_HttpTest($hash,$id,"PUT",$data);
  625. return "+1+$data+2+$err+3+\n";
  626. } elsif($value =~ /\s+raw$/) {
  627. $value =~ s/\s+raw$//g;
  628. Log3 $name, 3, "$name set raw $id value $value";
  629. $data = BDKM_Encrypt($hash,$value);
  630. ($data,$err) = BDKM_HttpPUT($hash,$id,$data);
  631. return $data.$err;
  632. } else {
  633. Log3 $name, 3, "$name set raw $id value $value";
  634. my $rawdata = BDKM_GetId($hash,$id,"raw") or
  635. return "unable to set $id because the value can not be read";
  636. defined $hash->{IDS}{$id}{VALUE} or return "ID $id is unknown";
  637. my $h=$hash->{IDS}{$id};
  638. my $type=$h->{TYPE};
  639. defined $h->{WRITEABLE} and $h->{WRITEABLE} or return "ID $id is not writeable";
  640. my $allowed=defined $h->{ALLOWED} ? $h->{ALLOWED} : "";
  641. my $json={};
  642. if($type eq "floatValue") {
  643. $value =~ s/\"//;
  644. $value =~ /^-?\d+\.?\d*$/ or return "$id needs a float/integer value";
  645. Log3 $name, 3, "$name $id set floatValue $value";
  646. my $ok = (defined $extra_value{$id} and $extra_value{$id} eq $value);
  647. !$ok and defined $h->{MIN} and defined $h->{MAX} and
  648. ($value < $h->{MIN} || $value > $h->{MAX}) and
  649. return "allowed values for $id are: interger/float in range $h->{MIN} to $h->{MAX}";
  650. $json->{value} = ($value + 0.0); # make number from it!
  651. } elsif ($type eq "stringValue") {
  652. Log3 $name, 3, "$name $id set stringValue $value";
  653. $allowed and $allowed !~ /$value/ and
  654. return "allowed values for $id are: one of $allowed";
  655. $json->{value} = $value;
  656. Log3 $name, 3, "$name set $id float value $value";
  657. } elsif ($type eq "arrayData") { # RC300 only for /system/holidayModes/hm[1-5]/assignedTo
  658. Log3 $name, 3, "$name $id set arrayData $value";
  659. my @a=split(/\s+/,$value);
  660. foreach(@a) {
  661. $allowed and $allowed !~ /$_/ and
  662. return "allowed values for $id are: one or more of $allowed";
  663. }
  664. $json->{values} = \@a;
  665. }
  666. if ($type eq "switchProgram") {
  667. Log3 $name, 3, "$name $id set switchProgram $value";
  668. my $postid=$id;
  669. $postid =~ s|/\d-([A-z][A-z])$||;
  670. $data=BDKM_makeSwitchPointData($rawdata,$1, $value);
  671. $data =~ /setpoint/ or return $data;
  672. $data = BDKM_Encrypt($hash,$data);
  673. ($data,$err) = BDKM_HttpPUT($hash,$postid,$data);
  674. } else {
  675. $data = BDKM_encode_http_data($hash,$json);
  676. BDKM_msleep($hash->{READBACKDELAY});
  677. ($data,$err) = BDKM_HttpPUT($hash,$id,$data);
  678. }
  679. BDKM_msleep($hash->{READBACKDELAY});
  680. my $ret = BDKM_GetId($hash,$id);
  681. $ret ne $value and $id ne "/gateway/DateTimeteTime" and
  682. return "$name Unable to set +$value+ to $id (readback: +$ret+)";
  683. return $ret;
  684. }
  685. }
  686. sub BDKM_Get($@)
  687. {
  688. my ( $hash, $name, $id, $opt) = @_;
  689. # specials
  690. $id eq "INFO" and return BDKM_GetInfo($hash, $opt);
  691. if(defined $opt and $opt eq "rawforce") {
  692. $opt="raw";
  693. } else {
  694. if(!defined $hash->{IDS}{$id} and !defined $hash->{USERTOREAL}{$id}
  695. or (defined($opt) and $opt ne "raw" and $opt ne "json")) {
  696. # only print aliased and special commands (like INFO):
  697. my @getable=qw(INFO);
  698. push(@getable, keys %{$hash->{USERTOREAL}});
  699. return "Unknown argument $id, choose one of ".join(" ",@getable);
  700. }
  701. }
  702. return BDKM_GetId($hash, $id, $opt);
  703. }
  704. sub BDKM_GetId($@)
  705. {
  706. my ($hash,$id,$opt) = @_;
  707. my $name=$hash->{NAME};
  708. defined $hash->{USERTOREAL}{$id} and $id = $hash->{USERTOREAL}{$id};
  709. my $json;
  710. defined $opt or $opt = "";
  711. my $realid=$id;
  712. if($id =~ m|/\d-[MTWTFS][ouehrau]$|) {
  713. # one of our pseudo switch program id
  714. # map to a real gateway id
  715. ($realid) = BDKM_MapSwitchPrograms([$id]);
  716. }
  717. # blocking http get:
  718. my($err,$data, $httpheader) = BDKM_HttpGET($hash,$realid);
  719. if($err) {
  720. Log3 $name, 2, "$name unable to fetch ID $id - $err";
  721. return "$name unable to fetch ID $id - $err";
  722. } else {
  723. ($json,$data) = BDKM_decode_http_data($hash,$data);
  724. Log3 $name, 2, "$name get $id - HTTP: $httpheader, data: $data";
  725. if($json) {
  726. BDKM_update_id_from_json($hash,$json);
  727. # always check for reading update when id was read
  728. BDKM_UpdateReadings($hash,[$id]);
  729. $opt eq "json" and return $json;
  730. }
  731. if($opt eq "raw") {
  732. return $data;
  733. }
  734. defined $hash->{IDS}{$id}{VALUE} and return $hash->{IDS}{$id}{VALUE};
  735. }
  736. return "";
  737. }
  738. # this routine takes the raw http json data of a switch program,
  739. # the week day and the setpointstring to be set like
  740. # "0700 comfort2 2200 eco"
  741. # It then patches the new setpoints for that day to the json data (sorted!).
  742. sub BDKM_makeSwitchPointData
  743. {
  744. my ($data, $weekday, $setpointstr) =@_;
  745. my @setpoints=();
  746. my $timeraster=0;
  747. map {
  748. s/\}.*//;
  749. /switchPointTimeRaster.*?(\d+)/ and $timeraster=$1;
  750. /setpoint[^A-z]/ and !/\"$weekday\"/ and push(@setpoints,'{'.$_.'}');
  751. } split(/{/,$data);
  752. my @a=split(/\s+/,$setpointstr);
  753. while(@a) {
  754. $_=shift(@a);
  755. s|:||;
  756. my ($hr,$min)=/^(\d\d)(\d\d)$/ or return "invalid time format - use: HHMM";
  757. ($hr > 23 or $min > 59) and return "$hr$min use a valid time between 0000 and 2359";
  758. $timeraster and $min % $timeraster and
  759. return "switch point $_ not allowed: switchpoint raster is 15 minutes";
  760. @a or return "$hr$min missing set point type";
  761. $_=shift(@a);
  762. my $time = $hr*60+$min;
  763. push(@setpoints, '{"dayOfWeek":"'.$weekday.'","setpoint":"'.$_.'","time":'.$time.'}'); # add weekday
  764. }
  765. return '['.join(',',
  766. sort {
  767. # by day num
  768. $WdToNum{($a =~ /dayOfWeek[^A-z]+([A-z][A-z])/)[0]} <=>
  769. $WdToNum{($b =~/dayOfWeek[^A-z]+([A-z][A-z])/)[0]} ||
  770. # and time
  771. ($a =~ /time[^\d]+(\d+)/)[0] <=>
  772. ($b =~ /time[^\d]+(\d+)/)[0]
  773. } @setpoints).']';
  774. }
  775. ############################## Helpers ###################################
  776. sub BDKM_HttpPostOrGet
  777. {
  778. my ($hash,$id,$method,$data,$callback) = @_;
  779. my $param = {
  780. url => "http://" . $hash->{IP} . $id,
  781. hash => $hash,
  782. data => $data,
  783. method => $method,
  784. header => "agent: PortalTeleHeater/2.2.3\r\nUser-Agent: TeleHeater/2.2.3\r\nAccept: application/json",
  785. };
  786. if(defined($callback)) {
  787. Log3 $hash, 5, "$hash->{NAME} async $method $param->{url}";
  788. $param->{timeout} = $hash->{HTTPTIMEOUT};
  789. $param->{callback} = $callback;
  790. HttpUtils_NonblockingGet($param);
  791. return undef;
  792. } else {
  793. $param->{timeout} = 3;
  794. Log3 $hash, 5, "$hash->{NAME} sync $method $param->{url}";
  795. return (HttpUtils_BlockingGet($param),$param->{httpheader}); #returns ($err, $data)
  796. }
  797. }
  798. sub BDKM_HttpGET
  799. {
  800. my ($hash,$id,$callback) = @_;
  801. return BDKM_HttpPostOrGet($hash,$id,"GET",undef,$callback);
  802. }
  803. sub BDKM_HttpPOST
  804. {
  805. my ($hash,$id,$data,$callback) = @_;
  806. return BDKM_HttpPostOrGet($hash,$id,"POST",$data,$callback);
  807. }
  808. sub BDKM_HttpPUT
  809. {
  810. my ($hash,$id,$data,$callback) = @_;
  811. return BDKM_HttpPostOrGet($hash,$id,"PUT",$data,$callback);
  812. }
  813. sub BDKM_Timer
  814. {
  815. my ($hash,$secs,$callback) = @_;
  816. InternalTimer(gettimeofday()+$secs, $callback, $hash, 0);
  817. }
  818. sub BDKM_RemoveTimer
  819. {
  820. RemoveInternalTimer($_[0]);
  821. }
  822. sub BDKM_Decrypt($$)
  823. {
  824. my ($hash, $data) = @_;
  825. $data = decode_base64($data);
  826. length($data) & 0xF and return ""; # must be 16byte blocked. if not decryt calls exit()!!!
  827. $data = $hash->{CRYPT}->decrypt( $data );
  828. my $len = length($data);
  829. ($len & 0xF) and return $data;
  830. # 16 byte block, remove padding
  831. my $i;
  832. for($i=0; $i < $len && ord(substr($data,-1-$i,1)) == 0; $i++){};
  833. if($i) {
  834. return substr($data,0,$len-$i);
  835. } else {
  836. # 16byte blocks not zero padded
  837. # check if RFC PKCS #7 padded and remove padding
  838. my $padchar = substr($data,($len - 1),1); #last char
  839. my $num = ord($padchar);
  840. if($num <= 16) {
  841. substr($data,$len - $num, $num) eq ($padchar x $num) and
  842. return substr($data,0,$len - $num);
  843. }
  844. }
  845. return $data;
  846. }
  847. sub BDKM_Encrypt($$)
  848. {
  849. my ($hash, $data) = @_;
  850. my $crypt = $hash->{CRYPT};
  851. my $blocksize = $crypt->blocksize();
  852. # pad data to block size before encrypting - see RFC 5652
  853. my $numpad = $blocksize - length($data)%$blocksize;
  854. return
  855. encode_base64($crypt->encrypt($data.(chr($numpad) x $numpad)));
  856. }
  857. sub BDKM_msleep
  858. {
  859. select(undef, undef, undef, $_[0]/1000);
  860. }
  861. sub BDKM_decode_http_data
  862. {
  863. my ($hash, $data) = @_;
  864. my $json="";
  865. Log3 $hash, 6, "$hash->{NAME} raw crypted HTTP data: $data";
  866. $data =~ /^\s*$/s and return ("","");
  867. $data = BDKM_Decrypt($hash,$data);
  868. my $len = length($data);
  869. Log3 $hash, 4, "$hash->{NAME} deocded $len bytes HTTP data: $data";
  870. if($data) {
  871. eval {$json = decode_json(encode_utf8($data)); 1; } or do {
  872. $json="";
  873. }
  874. }
  875. return ($json,$data);
  876. }
  877. sub BDKM_encode_http_data
  878. {
  879. my ($hash, $json) = @_;
  880. my $data = encode_json($json);
  881. Log3 $hash, 3, "$hash->{NAME} raw HTTP data: $data";
  882. $data = BDKM_Encrypt($hash,$data);
  883. Log3 $hash, 6, "$hash->{NAME} encocded HTTP data: $data";
  884. return $data;
  885. }
  886. sub BDKM_update_id_from_json
  887. {
  888. my ($hash,$json) = @_;
  889. if ($json) {
  890. my $id = $json->{id};
  891. my $type = $json->{type};
  892. Log3 $hash, 6, "$hash->{NAME} update JSON $id $type";
  893. defined ($hash->{IDS}{$id}) or $hash->{IDS}{$id}={};
  894. my $h = $hash->{IDS}{$id};
  895. if($type eq "stringValue" or $type eq "floatValue"){
  896. if(!defined($h->{WRITEABLE})) { # initial
  897. $h->{WRITEABLE} = $json->{writeable};
  898. $h->{TYPE}=$type;
  899. defined($json->{unitOfMeasure}) and $h->{UNIT} = $json->{unitOfMeasure};
  900. defined($json->{minValue}) and $h->{MIN} = $json->{minValue};
  901. defined($json->{maxValue}) and $h->{MAX} = $json->{maxValue};
  902. defined($json->{allowedValues}) and $h->{ALLOWED} = join(" ",@{$json->{allowedValues}});
  903. }
  904. $h->{VALUE} = $json->{value};
  905. } elsif ($type eq "switchProgram") {
  906. my @prog=();
  907. my $weekday;
  908. foreach my $sp (@{$json->{switchPoints}}) {
  909. $weekday = $sp->{dayOfWeek};
  910. my $t = $sp->{time};
  911. my $h = int($t/60);
  912. my $entry = sprintf("%02d%02d %s",$h,$t-($h*60),$sp->{setpoint});
  913. my $num = $WdToNum{$weekday};
  914. $prog[$num] = defined $prog[$num] ? $prog[$num]." ".$entry : $entry;
  915. Log3 $hash, 5, "$hash->{NAME} update switchProgram $weekday $entry $sp->{time}";
  916. }
  917. my $i=1;
  918. foreach $weekday (qw(Mo Tu We Th Fr Sa Su)) {
  919. my $newid = "$id/$i-".$weekday;
  920. if(!defined $hash->{IDS}{$newid}) {
  921. $hash->{IDS}{$newid}={
  922. ID => $newid,
  923. TYPE => $type,
  924. WRITEABLE => 1
  925. }
  926. }
  927. $hash->{IDS}{$newid}{VALUE} = $prog[$i++];
  928. }
  929. } elsif ($type eq "errorList") {
  930. ### Sort list by timestamps
  931. my $err="";
  932. if(defined $json->{values}) {
  933. foreach my $entry (sort ( @{$json->{values}} )) {
  934. $err .= sprintf("%-20.20s %-3.3s %-4.4s %-2.2s\n",
  935. $entry->{t}, $entry->{dcd}, $entry->{ccd}, $entry->{cat});
  936. }
  937. }
  938. if(!defined($h->{WRITEABLE})) { # initial
  939. $h->{WRITEABLE} = $json->{writeable};
  940. $h->{TYPE} = "arrayData"; #is also arraydata
  941. }
  942. $h->{VALUE} = $err;
  943. } elsif ($type eq "systeminfo" or $type eq "arrayData") {
  944. my $info="";
  945. if(defined $json->{values}) {
  946. foreach my $val (@{$json->{values}}) {
  947. if(ref($val) eq 'HASH') {
  948. $info .=join(" ", map { $_.":".$val->{$_} } keys %{$val})." ";
  949. } else {
  950. $info .= $val." ";
  951. }
  952. }
  953. $info =~ s/ $//;
  954. }
  955. if(!defined($h->{WRITEABLE})) { # initial
  956. defined($json->{allowedValues}) and $h->{ALLOWED} = join(" ",@{$json->{allowedValues}});
  957. $h->{WRITEABLE} = $json->{writeable};
  958. $h->{TYPE}= "arrayData"; # info is also arraydata
  959. }
  960. $h->{VALUE} = $info;
  961. } elsif ($type eq "yRecording") {
  962. defined $h->{TYPE} or $h->{TYPE}="Recroding";
  963. # ignore recordings - fhem records :-)
  964. } elsif ($type eq "refEnum") {
  965. # ignore directory entry
  966. } elsif ($type eq "eMonitoringList") {
  967. # ignore eMonitoringList - I don't have infos about that
  968. } else {
  969. Log3 $hash, 2, "$hash->{NAME}: unknown type $type for $id";
  970. }
  971. } else {
  972. Log3 $hash, 5, "$hash->{NAME}: no JSON data available";
  973. }
  974. }
  975. sub BDKM_MapSwitchPrograms
  976. {
  977. # translate all /dhwCircuits/dhw1/switchPrograms/A/\d-[A-z][A-z]$ forms to
  978. # one real reading like /dhwCircuits/dhw1/switchPrograms/A
  979. my $aref=$_[0];
  980. my %seen=();
  981. my $x;
  982. # substitution needs $x becaus because $_ would modify original array!!
  983. return grep {!$seen{$_}++} map {$x=$_; $x =~ s|/\d-[MTWTFS][ouehrau]$||;$x} @$aref;
  984. }
  985. 1;
  986. # perl ./contrib/commandref_join.pl FHEM/79_BDKM.pm
  987. # perl ./contrib/commandref_join.pl
  988. =pod
  989. =item device
  990. =item summary support for Buderus KM-Gateways
  991. =item summary_DE Unterst&uuml;tzung f&uuml;r Buderus KM-Gateways
  992. =begin html
  993. <a name="BDKM"></a>
  994. <h3>BDKM</h3>
  995. <ul>
  996. BDKM is a module supporting Buderus Logamatic KM gateways similar
  997. to the <a href="#km200">km200</a> module. For installation of the
  998. gateway see fhem km200 internet wiki<br>
  999. Compared with the km200 module the code of the BDKM module is more
  1000. compact and has some extra features. It has the ablility to
  1001. define how often a gateway ID is polled, which FHEM reading
  1002. (alias) is generated for a gateway ID and which minimum difference
  1003. to the last reading must exist to generate a new reading (see
  1004. attributes).<br>
  1005. It determines value ranges, allowed values and writeability from
  1006. the gateway supporting FHEMWEB and readingsGroup when setting
  1007. Values (drop down value menues).<br>
  1008. On definition of a BDKM device the gateway is connected and a full
  1009. poll collecting all IDs is done. This takes about 20 to 30
  1010. seconds. After that the module knows all IDs reported
  1011. by the gateway. To examine these IDs just type:<BR>
  1012. <code>get myBDKM INFO</code><BR>
  1013. These IDs can be used with the PollIds attribute to define if and
  1014. how the IDs are read during the poll cycle. <br> All IDs can be
  1015. mapped to own short readings.
  1016. <br><br>
  1017. <a name="BDKMdefine"></a>
  1018. <b>Define</b>
  1019. <ul>
  1020. <code>define &lt;name&gt; BDKM &lt;IP-address|hostname&gt; &lt;GatewayPassword&gt;
  1021. &lt;PrivatePassword&gt; &lt;MD5-Salt&gt;</code><br>
  1022. or <br>
  1023. <code>define &lt;name&gt; BDKM &lt;IP-address|hostname&gt; &lt;AES-Key&gt;</code><br>
  1024. <br><br>
  1025. <code>&lt;name&gt;</code> :
  1026. Name of device<br>
  1027. <code>&lt;IP-address&gt;</code> :
  1028. The IP adress of your Buderus gateway<br>
  1029. <code>&lt;GatewayPassword&gt;</code> :
  1030. The gateway password as printed on case of the gateway s.th.
  1031. of the form: xxxx-xxxx-xxxx-xxxx<br>
  1032. <code>&lt;PrivatePassword&gt;</code> : The private password as
  1033. set with the buderus App<br>
  1034. <code>&lt;MD5-Salt&gt;</code> : MD5 salt for the crypt
  1035. algorithm you want to use (hex string like 867845e9.....). Have a look for km200 salt 86 ... <br>
  1036. AES-Key can be generated here:<br>
  1037. https://ssl-account.com/km200.andreashahn.info<br>
  1038. <br>
  1039. </ul>
  1040. <a name="BDKMset"></a>
  1041. <b>Set </b>
  1042. <ul>
  1043. <code>set &lt;name&gt; &lt;ID&gt; &lt;value&gt; ...</code>
  1044. <br><br>
  1045. where <code>ID</code> is a valid writeable gateway ID (See list command,
  1046. or "<code>get myBDKM INFO</code>")<br>
  1047. The set command first reads the the ID from the gateway and also
  1048. triggers a FHEM readings if necessary. After that it is checked if the
  1049. value is valid. Then the ID and value(s) are transfered to to the
  1050. gateway. After waiting (attr ReadBackDelay milliseconds) the value
  1051. is read back and checked against value to be set. If necessary again
  1052. a FHEM reading may be triggered. The read back value or an error is
  1053. returned by the command. <br>
  1054. Examples:
  1055. <ul>
  1056. <code>set myBDKM /heatingCircuits/hc1/temporaryRoomSetpoint 22.0</code><br>
  1057. or the aliased version of it (if
  1058. /heatingCircuits/hc1/temporaryRoomSetpointee is aliased to
  1059. RoomTemporaryDesiredTemp):<br>
  1060. <code>set myBDKM RoomTemporaryDesiredTemp 22.0</code><br>
  1061. special to set time of gateway to the hosts date:<br>
  1062. <code>set myBDKM /gateway/DateTime now</code><br>
  1063. aliased:<br>
  1064. <code>set myBDKM DateTime now</code><br>
  1065. </ul>
  1066. <br>
  1067. </ul>
  1068. <br>
  1069. <a name="BDKMget"></a>
  1070. <b>Get </b>
  1071. <ul>
  1072. <code>get &lt;name&gt; &lt;ID&gt; &lt;[raw|json]&gt;...</code><br><br>
  1073. where <code>ID</code> is a valid gateway ID or an alias to it.
  1074. (See list command)<br> The get command reads the the ID from the
  1075. gateway, triggeres readings if necessarry, and returns the value
  1076. or an error if s.th. went wrong. While polling is done
  1077. asychronously with a non blocking HTTP GET. The set and get
  1078. functions use a blocking HTTP GET/POST to be able to return a
  1079. value directly to the user. Normaly get and set are only used by
  1080. command line or when setting values via web interface.<br>
  1081. With the <code>raw</code> option the whole original decoded data of the
  1082. ID (as read from the gateway) is returned as a string.<br> With
  1083. the <code>json</code> option a perl hash reference pointing to the
  1084. JSON data is returned (take a look into the module if you want to
  1085. use that)<br>
  1086. <br>
  1087. Examples:
  1088. <ul>
  1089. <code>get myBDKM /heatingCircuits/hc1/temporaryRoomGetpoint</code><br>
  1090. or the aliased version of it (see attr below):<br>
  1091. <code>get myBDKM RoomTemporaryDesiredTemp</code><br>
  1092. <code>get myBDKM DateTime</code><br>
  1093. <code>get myBDKM /gateway/instAccess</code><br>
  1094. Spacial to get Infos about IDs known by the gateway and own
  1095. configurations:<BR>
  1096. <code>get myBDKM INFO</code><br>
  1097. Everything matching /temp/
  1098. <code>get myBDKM INFO temp</code><br>
  1099. Everything matching /Heaven/ or /Hell/
  1100. <code>get myBDKM INFO Heaven Hell</code><br>
  1101. Everything known:
  1102. <code>get myBDKM INFO .*</code><br>
  1103. Arguments to <code>INFO</code> are reqular expressions
  1104. which are matched against all IDs and all aliases.
  1105. </ul>
  1106. <br>
  1107. </ul>
  1108. <br>
  1109. <a name="BDKMattr"></a>
  1110. <b>Attributes</b>
  1111. <ul>
  1112. <li>BaseInterval<br>
  1113. The interval time in seconds between poll cycles.
  1114. It defaults to 120 seconds. Which means that every 120 seconds a
  1115. new poll collects values of IDs which turn it is.
  1116. </li><br>
  1117. <li>InterPollDelay<br>
  1118. The delay time in milliseconds between reading of two IDs from
  1119. the gateway. It defaults to 0 (read as fast as possible).
  1120. Some gateways/heatings seem to stop answering after a while
  1121. when you are reading a lot of IDs. (verbose 2 "communication ERROR").
  1122. To avoid gateway hangups always try to read only as many IDs as
  1123. really required. If it doesn't help try to increase the
  1124. InterPollDelay value. E.g. start with 100.
  1125. </li><br>
  1126. <li>ReadBackDelay<br>
  1127. Read back delay for the set command in milliseconds. This value
  1128. defaults to 500 (0.5s). After setting a value, the gateway need
  1129. some time before the value can be read back. If this delay is
  1130. too short after writing you will get back the old value and not
  1131. the expected new one. The default should work in most cases.
  1132. </li><br>
  1133. <li>HttpTimeout<br>
  1134. Timeout for all HTTP requests in seconds (polling, set,
  1135. get). This defaults to 10s. If there is no answer from the
  1136. gateway for HttpTimeout time an error is returned. If a HTTP
  1137. request expires while polling an error log (level 2) is
  1138. generated and the request is automatically restarted after 60
  1139. seconds.
  1140. </li><br>
  1141. <li>PollIds<br>
  1142. Without this attribute FHEM readings are NOT generated
  1143. automatically! <br>
  1144. This attribute defines how and when IDs are polled within
  1145. a base interval (set by atrribute <code>BaseInterval</code>).<br>
  1146. The attribute contains list of space separated IDs and options
  1147. written as <br>
  1148. <code>GatewayID:Modulo:Delta:Alias</code>
  1149. <br>
  1150. Where Gateway is the real gateway ID like "/gateway/DateTime".<br>
  1151. Modulo is the value which defines how often the GatewayID is
  1152. polled from the gateway and checked for FHEM readings update.
  1153. E.g. a value of 4 means that the ID is polled only every 4th cycle.<br>
  1154. Delta defines the minimum difference a polled value must have to the
  1155. previous reading, before a FHEM reading with the new value is generated.<br>
  1156. Alias defines a short name for the GatewayID under which the gateway ID
  1157. can be accessed. Also readings (Logfile entries) are generated with this
  1158. short alias if set. If not set, the original ID is used.<br>
  1159. In detail:<br>
  1160. <code>ID:1:0:Alias</code> - poll every cycle, when difference >= 0 to previous reading (means always, also for strings) trigger FHEM reading to "Alias"<br>
  1161. <code>ID:1::Alias</code> - poll every cycle, no Delta set => trigger FHEM reading to "Alias" on value change only<br>
  1162. <code>ID:0::Alias</code> - update reading on startup once if reading changed (to the one prevously saved in fhem.save)<br>
  1163. <code>ID:1:0.5:Alias</code> - poll every cycle, when difference => 0.5 trigger a FHEM reading to "Alias"<br>
  1164. <code>ID:15::Alias</code> - poll every 15th cylce, update reading only if changed<br>
  1165. <code>ID:::Alias</code> - update reading on (get/set) only and only if value changed<br>
  1166. <code>ID::0:Alias</code> - update reading on (get/set) only and trigger reading always on get/set<br>
  1167. <code>ID</code> - without colons ":", poll every cycle, update reading allways (same as <code>ID:1:0:</code>)<br>
  1168. Also some usefull defaults can be set by the special keyword RC300DEFAULTS, RC35DEFAULTS, RC30DEFAULTS.<br>
  1169. As I don't know anything about RC35 or RC30 the later keywords are currently empty (please send me some info with "get myBDKM INFO" :-)<br>
  1170. Definitions set by the special keywords (see the module code for it) are overwritten by definitions later set in the attribute definition<br>
  1171. Example:
  1172. <ul>
  1173. <code>attr myBDKM PollIds \<br>
  1174. RC300DEFAULTS \<br>
  1175. /gateway/DateTime:0::Date \<br>
  1176. /system/info:0:0:\<br>
  1177. /dhwCircuits/dhw1/actualTemp:1:0.2:WaterTemp
  1178. </code><br>
  1179. </ul>
  1180. Which means: Use RC300DEFAULTS, trigger FHEM reading "Date" when date has changed on startup only. Trigger FHEM reading "/system/info" (no aliasing) always on startup, poll water temperature every cycle and trigger FHEM reading "WaterTemp" when difference to last reading was at least 0.2 degrees.
  1181. <br>
  1182. </li><br>
  1183. </ul>
  1184. </ul>
  1185. =end html