diff --git a/fhem/FHEM/98_ArduCounter.pm b/fhem/FHEM/98_ArduCounter.pm index acf4e6f26..ecd67d3f3 100755 --- a/fhem/FHEM/98_ArduCounter.pm +++ b/fhem/FHEM/98_ArduCounter.pm @@ -1,4 +1,4 @@ -############################################################################ +############################################################################# # $Id$ # fhem Modul für Impulszähler auf Basis von Arduino mit ArduCounter Sketch # @@ -85,34 +85,47 @@ # 2019-08-10 fix parsing of levels at devVerbose >= 25 # 2019-08-12 fix documentation of keepalive attributes, add parsing of RSSI, # add missing attributes if they don't match the running device config -# 2019-1ß-13 fix a bug where calc counters are not created when readingPulsesPerKWh$pinName is specified instead of readingPulsesPerKWh$pin +# 2019-10-13 fix a bug where calc counters are not created when readingPulsesPerKWh$pinName is specified instead of readingPulsesPerKWh$pin +# 2019-11-16 fix typos in documentation +# 2019-12-21 fix small bugs discovered while testing ESP32 based boards, enhanced documentation. +# +# 2020-01-29 Version 7.00, modifictaions for new firmware >= 4.00 +# 2020-04-17 V 7.24 - many things restructured, new attributes, new set options +# 2020-04-29 V 7.26 - fixed a small bug in handling the deviceDisplay attribute +# 2020-05-01 V 7.27 - fix rssi reading +# 2020-05-03 V 7.28 - fix board attr checking when sending display config +# 2020-05-08 V 7.29 - fix debug logging +# 2020-05-17 V 7.30 - fix pin / pinName usage in configureDisplay +# 2020-05-21 V 7.31 - documentation enhancements # # # ideas / todo: -# -# - optimize configureDevice (not set attributes / defaults dont match running config -> check all attrs ...) +# - check reply from device after sending a command +# - rename existing readings if new name is specified in attr # - max time for interpolation as attribute -# - detect level threasholds automatically for analog input, track drift -# -# - convert module to package -# -# - OTA Flashing for ESP -# -# - parse sequence num of history entries -> reconstruct long history list in perl mem -# and display with get history instead of readings incl. individual time -# +# - detect level thresholds automatically for analog input, track drift +# - convert module to package # - timeMissed # -# +# +# My house water meter: +# 36.4? pulses per liter +# +# one big tap open = 9l / min -> 0,5 qm / h +# Max 5qm / h theoretical max load +# = 83l/min = 1,6 l / sec => 50 pulses / sec = 50 Hz freq. +# => minimal duration 20 ms, sampling at 5ms is fine +# package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); +use DevIo; -my $ArduCounter_Version = '6.19 - 13.10.2019'; +my $ArduCounter_Version = '7.30 - 17.5.2020'; my %ArduCounter_sets = ( @@ -120,12 +133,17 @@ my %ArduCounter_sets = ( "enable" => "", "raw" => "", "reset" => "", + "resetWifi" => "", "flash" => "", "saveConfig" => "", "clearLevels" => "", + "counter" => "", + "clearCounters" => "", + "clearHistory" => "", "reconnect" => "" ); + my %ArduCounter_gets = ( "info" => "", "history" => "", @@ -144,33 +162,23 @@ my %AnalogPinMap = ( "A6" => 20, "A7" => 21 }, "ESP8266" => { - "A0" => 17 } -); -my %rAnalogPinMap = ( - "NANO" => { - 14 => "A0", - 15 => "A1", - 16 => "A2", - 17 => "A3", - 18 => "A4", - 19 => "A5", - 20 => "A6", - 21 => "A7" }, - "ESP8266" => { - 17 => "A0" } + "A0" => 17 }, + "ESP32" => { + "A0" => 36 }, + "T-Display" => { + "A0" => 36 } ); +my %rAnalogPinMap; -# + +######################################################################### # FHEM module intitialisation # defines the functions to be called from FHEM -######################################################################### sub ArduCounter_Initialize($) { my ($hash) = @_; - require "$attr{global}{modpath}/FHEM/DevIo.pm"; - $hash->{ReadFn} = "ArduCounter_Read"; $hash->{ReadyFn} = "ArduCounter_Ready"; $hash->{DefFn} = "ArduCounter_Define"; @@ -180,13 +188,22 @@ sub ArduCounter_Initialize($) $hash->{AttrFn} = "ArduCounter_Attr"; $hash->{NotifyFn} = "ArduCounter_Notify"; $hash->{AttrList} = - 'board:UNO,NANO,ESP8266 ' . - 'pin[AD]?[0-9]+ ' . - 'interval ' . + 'board:UNO,NANO,ESP8266,ESP32,T-Display ' . + 'pin[AD]?[0-9]+ ' . # configuration of pins -> sent to device + 'interval ' . # configuration of intervals -> sent to device 'factor ' . # legacy (should be removed, use pulsesPerKwh instead) - 'pulsesPerKWh ' . - 'devVerbose:0,5,10,20 ' . # verbose level of board - 'analogThresholds ' . + 'pulsesPerKWh ' . # old + 'pulsesPerUnit ' . + 'flowUnitTime ' . # time for which the flow / consumtion is calculated. Defaults to 3600 seconds (one hour) + + 'devVerbose:0,5,10,20,30,40,50 ' . # old configuration of verbose level of board -> sent to device + 'enableHistory:0,1 ' . # history creation on device + 'enableSerialEcho:0,1,2 ' . # serial echo of output via TCP from device + 'enablePinDebug:0,1 ' . # show pin state changes from device + 'enableAnalogDebug:0,1,2,3 ' . # show analog levels + 'enableDevTime:0,1 ' . # device will send its time so drift can be detected + + 'analogThresholds ' . # legacy (should be removed, add to pin attributes instead) 'readingNameCount[AD]?[0-9]+ ' . # raw count for this running period 'readingNamePower[AD]?[0-9]+ ' . 'readingNameLongCount[AD]?[0-9]+ ' . # long term count @@ -194,8 +211,12 @@ sub ArduCounter_Initialize($) 'readingNameCalcCount[AD]?[0-9]+ ' . # new to be implemented by using factor for the counter as well 'readingFactor[AD]?[0-9]+ ' . 'readingPulsesPerKWh[AD]?[0-9]+ ' . + 'readingPulsesPerUnit[AD]?[0-9]+ ' . + 'readingFlowUnitTime[AD]?[0-9]+ ' . # time for which the flow / consumtion is calculated. Defaults to 3600 seconds (one hour) 'readingStartTime[AD]?[0-9]+ ' . 'verboseReadings[AD]?[0-9]+ ' . + 'runTime[AD]?[0-9]+ ' . # keep runTime for this pin + 'runTimeIgnore[AD]?[0-9]+ ' . # ignore runTime for this pin while specified devices switched on 'flashCommand ' . 'helloSendDelay ' . 'helloWaitTime ' . @@ -207,18 +228,24 @@ sub ArduCounter_Initialize($) 'silentReconnect:0,1 ' . 'openTimeout ' . 'maxHist ' . - + 'deviceDisplay ' . + 'logFilter ' . 'disable:0,1 ' . 'do_not_notify:1,0 ' . $readingFnAttributes; - - # todo: create rAnalogPinMap hash from AnalogPinMap - + + foreach my $board (keys %AnalogPinMap) { + foreach my $pinName (keys %{$AnalogPinMap{$board}}) { + my $pin = $AnalogPinMap{$board}{$pinName}; + $rAnalogPinMap{$board}{$pin} = $pinName; + #Log3 undef, 3, "ArduCounter: initialize rAalogPinMap $board - $pin - $pinName"; + } + } } -# -# Define command + ########################################################################## +# Define command sub ArduCounter_Define($$) { my ($hash, $def) = @_; @@ -231,15 +258,17 @@ sub ArduCounter_Define($$) my $name = $a[0]; my $dev = $a[2]; - if ($dev =~ m/^(.+):([0-9]+)$/) { - # tcp conection + if ($dev =~ m/^(.+):([0-9]+)$/) { # tcp conection + $hash->{TCP} = 1; + } elsif ($dev =~ m/^(\d+\.\d+\.\d+\.\d+)(?:\:([0-9]+))?$/) { $hash->{TCP} = 1; + $dev .= ':80' if (!$2); } else { if ($dev !~ /.+@([0-9]+)/) { - $dev .= '@38400'; + $dev .= '@115200'; # add new default serial speed } else { - Log3 $name, 3, "$name: Warning: connection speed $1 is not the default for the ArduCounter firmware" - if ($1 != 38400); + Log3 $name, 3, "$name: Warning: connection speed $1 is not the default for the latest ArduCounter firmware" + if ($1 != 115200); } } $hash->{DeviceName} = $dev; @@ -248,21 +277,15 @@ sub ArduCounter_Define($$) $hash->{STATE} = "disconnected"; delete $hash->{Initialized}; # device might not be initialized - wait for hello / setup before cmds - - if(!defined($attr{$name}{'flashCommand'})) { - #$attr{$name}{'flashCommand'} = 'avrdude -p atmega328P -b 57600 -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]'; # for nano - $attr{$name}{'flashCommand'} = 'avrdude -p atmega328P -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]'; # for uno - } - + Log3 $name, 5, "$name: defined with $dev, Module version $ArduCounter_Version"; # do open in notify after init_done or after a new defined device (also after init_done) return; } -# -# undefine command when device is deleted ######################################################################### +# undefine command when device is deleted sub ArduCounter_Undef($$) { my ( $hash, $arg ) = @_; @@ -270,9 +293,9 @@ sub ArduCounter_Undef($$) } +##################################################### # remove timers, call DevIo_Disconnected # to set state and add to readyFnList -##################################################### sub ArduCounter_Disconnected($) { my $hash = shift; @@ -286,7 +309,8 @@ sub ArduCounter_Disconnected($) } -##################################### +##################################################### +# open callback sub ArduCounter_OpenCB($$) { my ($hash, $msg) = @_; @@ -298,7 +322,7 @@ sub ArduCounter_OpenCB($$) delete $hash->{BUSY_OPENDEV}; if ($hash->{FD}) { Log3 $name, 5, "$name: ArduCounter_Open succeeded in callback"; - my $hdl = AttrVal($name, "helloSendDelay", 15); + my $hdl = AttrVal($name, "helloSendDelay", 4); # send hello if device doesn't say "Started" withing $hdl seconds RemoveInternalTimer ("sendHello:$name"); InternalTimer($now+$hdl, "ArduCounter_AskForHello", "sendHello:$name", 0); @@ -401,9 +425,8 @@ sub ArduCounter_Ready($) ####################################################### -# Aufruf aus InternalTimer -# falls in Parse TCP Connection wieder abgewiesen wird -# weil "already busy" +# called from InternalTimer +# if TCP connection is busy or after firmware flash sub ArduCounter_DelayedOpen($) { my $param = shift; @@ -439,7 +462,8 @@ sub ArduCounter_Notify($$) return; } - Log3 $name, 3, "$name: Notify called with events: @{$events}, open device and set timer to send hello to device"; + Log3 $name, 3, "$name: Notify called with events: @{$events}, " . + "open device and set timer to send hello to device"; ArduCounter_Open($hash); } @@ -467,16 +491,351 @@ sub ArduCounter_Write ($$) ########################################################### -# return the name of the caling function for debug output +# return the name of the calling function for debug output sub ArduCounter_Caller() { my ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, $hints, $bitmask, $hinthash) = caller 2; return $1 if ($subroutine =~ /main::ArduCounter_(.*)/); return $1 if ($subroutine =~ /main::(.*)/); + return 'Fhem internal timer' if ($subroutine =~ /main::HandleTimeout/); return "$subroutine"; } +######################################################################################## +# return the internal pin number for an analog pin name name like A1 +# $hash->{Board} is set in parseHello and potentially overwritten by Attribut board +# called from Attr and ConfigureDevice to translate analog pin specifications to numbers +# in all cases Board and AllowedPins have been received with hello before +sub ArduCounter_PinNumber($$) +{ + my ($hash, $pinName) = @_; + my $name = $hash->{NAME}; + my $board = $hash->{Board}; + my $pin; + + if (!$board) { # if board is not known, try to guess it + # maybe no hello received yet and no Board-attr set (should never be the case) + my @boardOptions = keys %AnalogPinMap; + my $count = 0; + foreach my $candidate (@boardOptions) { + if ($AnalogPinMap{$candidate}{$pinName}) { + $board = $candidate; + $count++; + } + } + if ($count > 1) { + Log3 $name, 3, "$name: PinNumber called from " . ArduCounter_Caller() . + " can not determine internal pin number for $pinName," . + " board type is not known (yet) and attribute Board is also not set"; + } elsif (!$count) { + Log3 $name, 3, "$name: PinNumber called from " . ArduCounter_Caller() . + " can not determine internal pin number for $pinName." . + " No known board seems to support it"; + } else { + Log3 $name, 3, "$name: PinNumber called from " . ArduCounter_Caller() . + " does not know what kind of board is used. " . + " Guessing $board ..."; + } + } + $pin = $AnalogPinMap{$board}{$pinName} if ($board); + if ($pin) { + Log3 $name, 5, "$name: PinNumber called from " . ArduCounter_Caller() . + " returns $pin for $pinName"; + } else { + Log3 $name, 5, "$name: PinNumber called from " . ArduCounter_Caller() . + " returns unknown for $pinName"; + } + return $pin # might be undef +} + + +###################################################### +# return the the pin as it is used in a pin attr +# e.g. D2 or A1 for a passed pin number +sub ArduCounter_PinName($$) +{ + my ($hash, $pin) = @_; + my $name = $hash->{NAME}; + + my $pinName = $pin; # start assuming that attrs are set as pinX + if (!AttrVal($name, "pin$pinName", 0)) { # if not + if (AttrVal($name, "pinD$pin", 0)) { # is the pin defined as pinDX? + $pinName = "D$pin"; + #Log3 $name, 5, "$name: using attrs with pin name D$pin"; + } elsif ($hash->{Board}) { + my $aPin = $rAnalogPinMap{$hash->{Board}}{$pin}; + if ($aPin) { # or pinAX? + $pinName = "$aPin"; + #Log3 $name, 5, "$name: using attrs with pin name $pinName instead of $pin or D$pin (Board $hash->{Board})"; + } + } + } + return $pinName; +} + +##################################################### +# return the first attr in the list that is defined +sub ArduCounter_AttrVal($$$;$$$) +{ + my ($hash, $default, $a1, $a2, $a3, $a4) = @_; + my $name = $hash->{NAME}; + return AttrVal($name, $a1, undef) if (defined (AttrVal($name, $a1, undef))); + #Log3 $name, 5, "$name: AAV (" . ArduCounter_Caller() . ") $a1 not there"; + return AttrVal($name, $a2, undef) if (defined ($a2) && defined (AttrVal($name, $a2, undef))); + #Log3 $name, 5, "$name: AAV (" . ArduCounter_Caller() . ") $a2 not there"; + return AttrVal($name, $a3, undef) if (defined ($a3) && defined (AttrVal($name, $a3, undef))); + #Log3 $name, 5, "$name: AAV (" . ArduCounter_Caller() . ") $a3 not there"; + return AttrVal($name, $a4, undef) if (defined ($a4) && defined (AttrVal($name, $a4, undef))); + #Log3 $name, 5, "$name: AAV (" . ArduCounter_Caller() . ") $a4 not there"; + return $default; +} + + +###################################################### +# return a meaningful name (the relevant reading name) +# for passed pin number +# called from functions that handle device output with pin number +sub ArduCounter_LogPinDesc($$) +{ + my ($hash, $pin) = @_; + my $pinName = ArduCounter_PinName ($hash, $pin); + return ArduCounter_AttrVal($hash, "pin$pin", "readingNameCount$pinName", "readingNameCount$pin", "readingNamePower$pinName", "readingNamePower$pin"); +} + + +###################################################### +# send 'a' command to the device to configure a pin +# called from attr (pin attribute) and configureDevice +# with a pinName +sub ArduCounter_ConfigurePin($$$) +{ + my ($hash, $pinArg, $aVal) = @_; + my $name = $hash->{NAME}; + my $opt; + + if ($aVal !~ /^(rising|falling)[ \,\;]*(pullup)?[ \,\;]*(min +)?(\d+)?(?:[ \,\;]*(?:analog )out *(\d+)(?:[ \,\;]*threshold *(\d+)[ \,\;]+(\d+)))?/) { + Log3 $name, 3, "$name: ConfigurePin got invalid config for $pinArg: $aVal"; + return "Invalid config for pin $pinArg: $aVal"; + } + my ($edge, $pullup, $minText, $min, $aout, $t1, $t2) = ($1, $2, $3, $4, $5, $6, $7); + if (!$hash->{Initialized}) { # no hello received yet + Log3 $name, 5, "$name: pin validation and communication postponed until device is initialized"; + return undef; # accept value but don't send it to the device yet. + } + my ($pin, $pinName) = ArduCounter_ParsePin($hash, $pinArg); + return "illegal pin $pinArg" if (!defined($pin)); # parsePin logs error if wrong pin spec + + if ($edge eq 'rising') {$opt = "3"} # pulse level rising or falling + elsif ($edge eq 'falling') {$opt = "2"} + $opt .= ($pullup ? ",1" : ",0"); # pullup + $opt .= ($min ? ",$min" : ",2"); # min length, default is 2 + if ($hash->{VersionFirmware} && $hash->{VersionFirmware} > "4.00") { + if (defined($aout)) { $opt .= ",$aout" } # analog out pin + if (defined($t2)) { $opt .= ",$t1,$t2" } # analog thresholds + } else { + Log3 $name, 3, "$name: ConfigurePin sends old syntax to outdated firmware ($hash->{VersionFirmware})"; + } + + Log3 $name, 3, "$name: ConfigurePin creates command ${pin},${opt}a"; + ArduCounter_Write($hash, "${pin},${opt}a"); # initialized is already checked above + return undef; +} + + +###################################################### +# send 'a' command to the device to configure a pin +sub ArduCounter_ConfigureIntervals($;$) +{ + my ($hash, $aVal) = @_; + my $name = $hash->{NAME}; + my $cmd; + + if (!defined($aVal)) { + $aVal = AttrVal($name, "interval", ""); + } + if ($aVal !~ /^(\d+)[\s\,](\d+)[\s\,]?(\d+)?[\s\,]?(\d+)?([\s\,](\d+)[\s\,]+(\d+))?/) { + Log3 $name, 3, "$name: Invalid interval specification $aVal"; + return "Invalid interval specification $aVal"; + } + my ($min, $max, $sml, $cnt, $ain, $asm) = ($1, $2, $3, $4, $5, $6, $7); + if ($min < 1 || $min > 3600 || $max < $min || $max > 3600) { + Log3 $name, 3, "$name: Invalid value in interval specification $aVal"; + return "Invalid Value $aVal"; + } + if (!$hash->{Initialized}) { + Log3 $name, 5, "$name: communication postponed until device is initialized"; + return undef; + } + $sml = 0 if (!$sml); + $cnt = 0 if (!$cnt); + $ain = 50 if (!$ain); + $asm = 4 if (!$asm); + + if ($hash->{VersionFirmware} && $hash->{VersionFirmware} > "4.00") { + $cmd = "${min},${max},${sml},${cnt},${ain},${asm}i"; + } else { + $cmd = "${min},${max},${sml},${cnt}i"; + } + Log3 $name, 3, "$name: ConfigureIntervals creates command $cmd"; + ArduCounter_Write($hash, $cmd); + return undef; +} + + +###################################################### +# send 'a' command to the device to configure a pin +sub ArduCounter_ConfigureVerboseLevels($;$$$$$) +{ + my ($hash, $eHist, $eSerial, $pinDebug, $aDebug, $eTime) = @_; + my $name = $hash->{NAME}; + my $err; + if (defined($eHist)) { + if ($eHist !~ /^[01]$/) { + $err = "illegal value for enableHistory: $eHist, only 0 and 1 allowed"; + } + } else { + $eHist = AttrVal($name, "enableHistory", 0); + } + if (defined($eSerial)) { + if ($eSerial !~ /^[012]$/) { + $err = "illegal value enableSerialEcho: $eSerial, only 0,1 and 2 allowed"; + } + } else { + $eSerial = AttrVal($name, "enableSerialEcho", 0); + } + if (defined($pinDebug)) { + if ($pinDebug !~ /^[01]$/) { + $err = "illegal value enablePinDebug: $pinDebug, only 0 and 1 allowed"; + } + } else { + $pinDebug = AttrVal($name, "enablePinDebug", 0); + } + if (defined($aDebug)) { + if ($aDebug !~ /^[0123]$/) { + $err = "illegal value enable AnalogDebug: $aDebug, only 0-3 allowed"; + } + } else { + $aDebug = AttrVal($name, "enableAnalogDebug", 0); + } + if (defined($eTime)) { + if ($eTime !~ /^[01]$/) { + $err = "illegal value enableDevTime: $eTime, only 0 and 1 allowed"; + } + } else { + $eTime = AttrVal($name, "enableDevTime", 0); + } + if ($err) { + Log3 $name, 3, "$name: $err"; + return $err; + } + if (!$hash->{Initialized}) { # no hello received yet + Log3 $name, 5, "$name: pin validation and communication postponed until device is initialized"; + return undef; # accept value but don't send it to the device yet. + } + + my $cmd = "${eHist},${eSerial},${pinDebug},${aDebug},${eTime}v"; + Log3 $name, 3, "$name: ConfigureVerboseLevels creates command $cmd"; + ArduCounter_Write($hash, $cmd); + return undef; +} + + +###################################################### +# encode string as int sequence +# used in communication with device +sub ArduCounter_IntString($) +{ + my ($inStr) = @_; + my $byteNum = 0; + my $val = 0; + my $outStr; + foreach my $char (split (//, $inStr)) { + if ($byteNum) { + $val = ord($char) * 256 + $val; # second char -> add as high byte + $outStr .= ",$val"; + $byteNum = 0; + } else { + $val = ord($char); # first char + $byteNum++; + } + } + if ($byteNum) { # low order byte has been set, high byte is still zero + $outStr .= ",$val"; # but not added to outstr yet + + } else { # high byte is used as well, + $outStr .= ",0"; # add training zero if val is not already zero + } + return $outStr; +} + + +###################################################################### +# send 'p' command to the device to configure +# a tft display connected to the device +# called from configureDevice which handles hello message from device +# and from attr deviceDisplay with $aVal +sub ArduCounter_ConfigureDisplay($;$) +{ + my ($hash, $aVal) = @_; + my ($pinArg, $pin, $pinName, $ppu, $fDiv, $unit, $fut, $funit); + my $name = $hash->{NAME}; + if (!defined($aVal)) { + $aVal = AttrVal($name, "deviceDisplay", ""); + } + if ($aVal =~ /^([AD\d]+)(?:[\s\,]+([^\s\,]+)(?:[\s\,]+([^\s\,]+)))\s*$/) { + ($pinArg, $unit, $funit) = ($1, $2, $3); + + if (!$hash->{Initialized}) { # no hello received yet + Log3 $name, 5, "$name: pin validation and communication postponed until device is initialized"; + return undef; # accept value but don't send it to the device yet. + } + ($pin, $pinName) = ArduCounter_ParsePin($hash, $pinArg); + return "illegal pin $pinArg" if (!defined($pin)); # parsePin logs error if wrong pin spec + + $ppu = ArduCounter_AttrVal($hash, 0, "readingPulsesPerUnit$pinName", "readingPulsesPerUnit$pin"); + $ppu = ArduCounter_AttrVal($hash, 0, "readingPulsesPerKWh$pinName", "readingPulsesPerKWh$pin") if (!$ppu); + $ppu = ArduCounter_AttrVal($hash, 1, "pulsesPerUnit", "pulsesPerKWh") if (!$ppu); + $fut = ArduCounter_AttrVal($hash, 60, "readingFlowUnitTime$pinName", "readingFlowUnitTime$pin"); + Log3 $name, 3, "$name: ConfigureDisplay pin $pin / $pinName, ppu $ppu, fut $fut"; + if ($ppu =~ /(\.\d)/) { + $fDiv = 10 ** (length($1)-1); + $ppu = int($ppu * $fDiv); + } else { + $fDiv = 1; + } + } else { + Log3 $name, 3, "$name: Invalid device display configuration $aVal"; + return "Invalid device display configuration $aVal"; + } + Log3 $name, 3, "$name: ConfigureDisplay $pin, $ppu, $fDiv, $unit, $fut, $funit"; + if (!$hash->{Initialized}) { # no hello received yet + Log3 $name, 5, "$name: pin validation and communication postponed until device is initialized"; + return undef; # accept value but don't send it to the device yet. + } + my $cmd = "$pin,$ppu,$fDiv" . ArduCounter_IntString($unit) . ",$fut" . ArduCounter_IntString($funit) . "u"; + Log3 $name, 3, "$name: ConfigureDisplay creates command $cmd"; + ArduCounter_Write($hash, $cmd); + return undef; +} + + +####################################################### +# called from InternalTimer +# if relevant attr is changed +sub ArduCounter_DelayedConfigureDisplay($) +{ + my $param = shift; + my (undef,$name) = split(/:/,$param); + my $hash = $defs{$name}; + + Log3 $name, 5, "$name: call configureDisplay after delay"; + RemoveInternalTimer ("delayedcdisp:$name"); + ArduCounter_ConfigureDisplay($hash); +} + + + ####################################### # Aufruf aus InternalTimer # send "h" to ask for "Hello" since device didn't say "Started" so far - maybe it's still counting ... @@ -491,7 +850,7 @@ sub ArduCounter_AskForHello($) return if (!ArduCounter_Write( $hash, "h")); my $now = gettimeofday(); - my $hwt = AttrVal($name, "helloWaitTime", 3); + my $hwt = AttrVal($name, "helloWaitTime", 2); RemoveInternalTimer ("hwait:$name"); InternalTimer($now+$hwt, "ArduCounter_HelloTimeout", "hwait:$name", 0); $hash->{WaitForHello} = 1; @@ -505,9 +864,35 @@ sub ArduCounter_HelloTimeout($) my $param = shift; my (undef,$name) = split(/:/,$param); my $hash = $defs{$name}; - Log3 $name, 3, "$name: device didn't reply to h(ello). Is the right sketch flashed? Is speed set to 38400?"; delete $hash->{WaitForHello}; RemoveInternalTimer ("hwait:$name"); + + if ($hash->{DeviceName} !~ m/^(.+):([0-9]+)$/) { # not TCP + if (!$hash->{OpenRetries}) { + $hash->{OpenRetries} = 1; + } else { + $hash->{OpenRetries}++; + if ($hash->{OpenRetries}++ > 4) { + Log3 $name, 3, "$name: device didn't reply to h(ello). Is the right sketch flashed? Is serial speed set to 38400 or 115200 for firmware >4.0?"; + return; + } + } + Log3 $name, 5, "$name: HelloTimeout: DeviceName in hash is $hash->{DeviceName}"; + if ($hash->{DeviceName} !~ /(.+)@([0-9]+)(.*)/) { # no serial speed specified + $hash->{DeviceName} .= '@38400'; # should not happen (added during define) + Log3 $name, 3, "$name: device didn't reply to h(ello). No serial speed set. Is the right sketch flashed? Trying again with \@38400"; + } else { + if ($2 == 38400) { + $hash->{DeviceName} = "${1}\@115200${3}"; # now try 115200 if 38400 before + Log3 $name, 3, "$name: device didn't reply to h(ello). Is the right sketch flashed? Serial speed was $2. Trying again with \@115200"; + } else { + $hash->{DeviceName} = "${1}\@38400${3}"; # now try 38400 + Log3 $name, 3, "$name: device didn't reply to h(ello). Is the right sketch flashed? Serial speed was $2. Trying again with \@38400"; + } + } + Log3 $name, 5, "$name: HelloTimeout: DeviceName in hash is set to $hash->{DeviceName}"; + ArduCounter_Open($hash); # try again + } } @@ -524,18 +909,15 @@ sub ArduCounter_KeepAlive($) if (IsDisabled($name)) { return; } - my $kdl = AttrVal($name, "keepAliveDelay", 10); # next keepalive as timer my $kto = AttrVal($name, "keepAliveTimeout", 2); # timeout waiting for response - Log3 $name, 5, "$name: sending k(eepAlive) to device"; - ArduCounter_Write( $hash, "1,${kdl}k"); - + Log3 $name, 5, "$name: sending k(eepAlive) to device" if (AttrVal($name, "logFilter", "N") =~ "N"); + ArduCounter_Write( $hash, "1,${kdl}k"); RemoveInternalTimer ("alive:$name"); InternalTimer($now+$kto, "ArduCounter_AliveTimeout", "alive:$name", 0); $hash->{WaitForAlive} = 1; #Log3 $name, 5, "$name: keepAlive timeout timer set $kto"; - if ($hash->{TCP}) { RemoveInternalTimer ("keepAlive:$name"); InternalTimer($now+$kdl, "ArduCounter_KeepAlive", "keepAlive:$name", 0); # next keepalive @@ -553,21 +935,20 @@ sub ArduCounter_AliveTimeout($) my $hash = $defs{$name}; #Log3 $name, 5, "$name: AliveTimeout called"; delete $hash->{WaitForAlive}; - $hash->{KeepAliveRetries} = 0 if (!$hash->{KeepAliveRetries}); - + $hash->{KeepAliveRetries} = 0 if (!$hash->{KeepAliveRetries}); if (++$hash->{KeepAliveRetries} > AttrVal($name, "keepAliveRetries", 2)) { Log3 $name, 3, "$name: device didn't reply to k(eeepAlive), no retries left, setting device to disconnected"; ArduCounter_Disconnected($hash); # set to Disconnected but let _Ready try to Reopen + delete $hash->{KeepAliveRetries}; } else { Log3 $name, 3, "$name: device didn't reply to k(eeepAlive), count=$hash->{KeepAliveRetries}"; } } -# -# Send config commands after Board reported it is ready or still counting -# called from internal timer to give device the time to report its config first ########################################################################## +# Send config commands after Board reported it is ready or still counting +# called when parsing hello message from device sub ArduCounter_ConfigureDevice($) { my $param = shift; @@ -575,138 +956,33 @@ sub ArduCounter_ConfigureDevice($) my $hash = $defs{$name}; # todo: check if device got disconnected in the meantime! - my @runningPins = sort grep (/[\d]/, keys %{$hash->{runningCfg}}); - Log3 $name, 5, "$name: ConfigureDevice: pins in running config: @runningPins"; + #Log3 $name, 5, "$name: ConfigureDevice: pins in running config: @runningPins"; my @attrPins = sort grep (/pin([dDaA])?[\d]/, keys %{$attr{$name}}); - Log3 $name, 5, "$name: ConfigureDevice: pins from attrs: @attrPins"; - - CHECKS: { - # first check if device did send its config, then compare and send config if necessary - if (!$hash->{runningCfg}) { - Log3 $name, 5, "$name: ConfigureDevice: no running config received"; - last CHECKS; - } - Log3 $name, 5, "$name: ConfigureDevice: got running config - comparing"; - - my $iAttr = AttrVal($name, "interval", ""); - if (!$iAttr) { - $iAttr = "30 600 2 2"; - Log3 $name, 5, "$name: ConfigureDevice: interval attr not set - take default $iAttr"; - } - if ($iAttr =~ /^(\d+) (\d+) ?(\d+)? ?(\d+)?$/) { - my $iRCfg = ($hash->{runningCfg}{I} ? $hash->{runningCfg}{I} : ""); - my $iACfg = "$1 $2" . ($3 ? " $3" : " 0") . ($4 ? " $4" : " 0"); - Log3 $name, 5, "$name: ConfigureDevice: comparing intervals (>$iRCfg< vs >$iACfg< from attr)"; - if (!$iRCfg || $iRCfg ne $iACfg) { - Log3 $name, 5, "$name: ConfigureDevice: intervals don't match (>$iRCfg< vs >$iACfg< from attr)"; - if (AttrVal($name, "interval", "none") eq "none") { # set attr if no attr is set and running config doesn't match - CommandAttr(undef, "$name interval 30 600 2 2") - } - last CHECKS; - } - } else { - Log3 $name, 3, "$name: ConfigureDevice: can not compare against interval attr - wrong format"; - } - - my $vAttr = AttrVal($name, "devVerbose", ""); - if (!$vAttr) { - $vAttr = 0; - Log3 $name, 5, "$name: ConfigureDevice: devVerbose attr not set - take default $vAttr"; - } - my $vRCfg = ($hash->{runningCfg}{V} ? $hash->{runningCfg}{V} : 0); - Log3 $name, 5, "$name: ConfigureDevice: comparing devVerbose $vRCfg vs $vAttr from attr)"; - if ($vRCfg != $vAttr) { - Log3 $name, 5, "$name: ConfigureDevice: devVerbose don't match ($vRCfg vs $vAttr from attr)"; - if (!AttrVal($name, "devVerbose", "none" eq "none")) { # set attr if no attr is set and running config doesn't match - CommandAttr(undef, "$name devVerbose 0") - } - last CHECKS; - } - - my $tAttr = AttrVal($name, "analogThresholds", ""); - if (!$tAttr) { - Log3 $name, 3, "$name: ConfigureDevice: no analogThresholds attribute"; - } else { - if ($tAttr =~ /^(\d+) (\d+)/) { - my $tRCfg = ($hash->{runningCfg}{T} ? $hash->{runningCfg}{T} : ""); - my $tACfg = "$1 $2"; - Log3 $name, 5, "$name: ConfigureDevice: comparing analog Thresholds (>$tRCfg< vs >$tACfg< from attr)"; - if (!$tRCfg || ($tRCfg ne $tACfg)) { - Log3 $name, 5, "$name: ConfigureDevice: analog Thresholds don't match (>$tRCfg< vs >$tACfg< from attr)"; - last CHECKS; - } - } else { - Log3 $name, 3, "$name: ConfigureDevice: can not compare against analogThreshold attr - wrong format"; - } - } - - Log3 $name, 5, "$name: ConfigureDevice: matches so far - now compare pins"; - # interval config matches - now check pins as well - if (@runningPins != @attrPins) { - Log3 $name, 5, "$name: ConfigureDevice: number of defined pins doesn't match (@runningPins vs. @attrPins)"; - last CHECKS; - } - for (my $i = 0; $i < @attrPins; $i++) { - Log3 $name, 5, "$name: ConfigureDevice: compare pin $attrPins[$i] to $runningPins[$i]"; - $attrPins[$i] =~ /pin([dDaA])?([\d+]+)/; - my $type = $1; - my $aPinNum = $2; # pin number from attr - - $aPinNum = ArduCounter_PinNumber($hash, $type.$aPinNum) if ($type eq 'A'); - if (!$aPinNum) { # should never happen, because board type is known and pin was allowed - Log3 $name, 5, "$name: ConfigureDevice can not compare pin config for $attrPins[$i], internal pin number can not be determined"; - last CHECKS; - } - - last CHECKS if (!$hash->{runningCfg}{$aPinNum}); - Log3 $name, 5, "$name: ConfigureDevice: now compare $attr{$name}{$attrPins[$i]} to $hash->{runningCfg}{$aPinNum}"; - - last CHECKS if ($attr{$name}{$attrPins[$i]} !~ /^(rising|falling) ?(pullup)? ?([0-9]+)?/); - my $aEdge = $1; - my $aPull = ($2 ? $2 : "nop"); - my $aMin = ($3 ? $3 : ""); - last CHECKS if ($hash->{runningCfg}{$aPinNum} !~ /^(rising|falling|-) ?(pullup|nop)? ?([0-9]+)?/); - my $cEdge = $1; - my $cPull = ($2 ? $2 : ""); - my $cMin = ($3 ? $3 : ""); - - last CHECKS if ($aEdge ne $cEdge || $aPull ne $cPull || $aMin ne $cMin); - - } - Log3 $name, 5, "$name: ConfigureDevice: running config matches attributes"; - return; - } - # todo: check for additional pins also when rest matches (return above is too early) - - Log3 $name, 5, "$name: ConfigureDevice: now check for pins without attr in @runningPins"; + #Log3 $name, 5, "$name: ConfigureDevice: pins from attrs: @attrPins"; + + #Log3 $name, 5, "$name: ConfigureDevice: check for pins without attr in list: @runningPins"; my %cPins; # get all pins from running config in a hash to find out if one is not defined on fhem side for (my $i = 0; $i < @runningPins; $i++) { $cPins{$runningPins[$i]} = 1; #Log3 $name, 3, "$name: ConfigureDevice remember pin $runningPins[$i]"; } - # send attributes to arduino device. Just call ArduCounter_Attr again - Log3 $name, 3, "$name: ConfigureDevice: no match -> send config"; + Log3 $name, 3, "$name: ConfigureDevice: send config"; while (my ($aName, $val) = each(%{$attr{$name}})) { - if ($aName =~ /^(interval|devVerbose|analogThresholds)/) { - Log3 $name, 5, "$name: ConfigureDevice calls Attr with $aName $val"; - ArduCounter_Attr("set", $name, $aName, $val); - } elsif ($aName =~ /^pin([dDaA])?([\d+]+)/) { - my $type = $1; - my $num = $2; - my $aPinNum = $num; - $aPinNum = ArduCounter_PinNumber($hash, "A$num") if ($type =~ /[aA]/); + if ($aName =~ /^pin([DA])?([\d+]+)/) { # for each pin attr + my $type = ($1 ? $1 : ''); + my $aPinNum = $2; # if not overwritten for analog pins, we have already a number + my $pinName = $type.$aPinNum; + $aPinNum = ArduCounter_PinNumber($hash, $pinName) if ($type && $type eq 'A'); # if this is an analog pin specification translate it if ($aPinNum) { - delete $cPins{$aPinNum}; - #Log3 $name, 5, "$name: ConfigureDevice ignore pin $aPinNum"; - Log3 $name, 5, "$name: ConfigureDevice calls Attr with $aName $val"; - ArduCounter_Attr("set", $name, $aName, $val); + ArduCounter_ConfigurePin($hash, $pinName, $val); + delete $cPins{$aPinNum}; # this pin from running config has an attr } else { Log3 $name, 3, "$name: ConfigureDevice can not send pin config for $aName, internal pin number can not be determined"; } } } - if (%cPins) { + if (%cPins) { # remaining pins in running config without attrs my $pins = join ",", keys %cPins; Log3 $name, 5, "$name: ConfigureDevice: pins in running config without attribute in Fhem: $pins"; foreach my $pin (keys %cPins) { @@ -716,85 +992,109 @@ sub ArduCounter_ConfigureDevice($) } else { Log3 $name, 5, "$name: ConfigureDevice: no pins in running config without attribute in Fhem"; } + + ArduCounter_ConfigureIntervals($hash); + ArduCounter_ConfigureVerboseLevels($hash); + ArduCounter_ConfigureDisplay($hash) if ($hash->{Board} =~ /Display/); + ArduCounter_Write( $hash, "s"); # get new running config } -# Attr command ######################################################################### +# setzt userAttr-Attribute bei Regex-Attrs +sub ArduCounter_ManageUserAttr($$) +{ + my ($hash, $aName) = @_; + my $name = $hash->{NAME}; + my $modHash = $modules{$hash->{TYPE}}; + + # handle wild card attributes -> Add to userattr to allow modification in fhemweb + if (" $modHash->{AttrList} " !~ m/ ${aName}[ :;]/) { + # nicht direkt in der Liste -> evt. wildcard attr in AttrList + foreach my $la (split " ", $modHash->{AttrList}) { + $la =~ /^([^:;]+)(:?.*)$/; + my $vgl = $1; # attribute name in list - probably a regex + my $opt = $2; # attribute hint in list + if ($aName =~ /^$vgl$/) { # yes - the name in the list now matches as regex + # $aName ist eine Ausprägung eines wildcard attrs + addToDevAttrList($name, "$aName" . $opt); # create userattr with hint to allow change in fhemweb + #Log3 $name, 5, "$name: ManageUserAttr added attr $aName with $opt to userattr list"; + if ($opt) { + # remove old entries without hint + my $ualist = $attr{$name}{userattr}; + $ualist = "" if(!$ualist); + my %uahash; + foreach my $a (split(" ", $ualist)) { + if ($a !~ /^${aName}$/) { # no match -> existing entry in userattr list is attribute without hint + $uahash{$a} = 1; # put $a as key into the hash so it is kept in userattr later + } else { # match -> in list without attr -> remove + #Log3 $name, 5, "$name: ManageUserAttr removes attr $a without hint $opt from userattr list"; + } + } + $attr{$name}{userattr} = join(" ", sort keys %uahash); + } + } + } + } else { + # exakt in Liste enthalten -> sicherstellen, dass keine +* etc. drin sind. + if ($aName =~ /\|\*\+\[/) { + Log3 $name, 3, "$name: Atribute $aName is not valid. It still contains wildcard symbols"; + return "$name: Atribute $aName is not valid. It still contains wildcard symbols"; + } + } +} + + + +######################################################################### +# Attr command sub ArduCounter_Attr(@) { my ($cmd,$name,$aName,$aVal) = @_; # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value - my $hash = $defs{$name}; my $modHash = $modules{$hash->{TYPE}}; - - #Log3 $name, 5, "$name: Attr called with @_"; + Log3 $name, 5, "$name: Attr called with @_"; if ($cmd eq "set") { - if ($aName =~ /^pin([DA]?)(\d+)/) { - if (!$hash->{Initialized}) { # no hello received yet - Log3 $name, 5, "$name: pin validation and communication postponed until device is initialized"; - return undef; # accept attribute but don't send it to the device yet. - } - # board did send hello already and therefore allowedPins and Board should be set ... - my $pinType = $1; - my $pin = $2; - if ($hash->{allowedPins}) { # list of allowed pins received with hello - my %pins = map { $_ => 1 } split (/,/, $hash->{allowedPins}); - if ($init_done && %pins && !$pins{$pin}) { - Log3 $name, 3, "$name: Invalid pin in attr $name $aName $aVal"; - return "Invalid / disallowed pin specification $aName. The board reports $hash->{allowedPins} as allowed."; - } - } - $pin = ArduCounter_PinNumber($hash, $pinType.$pin) if ($pinType eq 'A'); - if (!$pin) { - # this should never happen since Board is known and Pin was already verified to be allowed. - Log3 $name, 3, "$name: can not determine internal pin number for attr $name $aName $aVal"; - return "pin specification is not valid or something went wrong. Check the logs"; - } - if ($aVal =~ /^(rising|falling) ?(pullup)? ?([0-9]+)?/) { - my $opt = ""; - if ($1 eq 'rising') {$opt = "3"} - elsif ($1 eq 'falling') {$opt = "2"} - $opt .= ($2 ? ",1" : ",0"); # pullup - $opt .= ($3 ? ",$3" : ""); # min length - - if ($hash->{Initialized}) { # hello already received - ArduCounter_Write($hash, "${pin},${opt}a"); - } else { - - } - - } else { - Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal"; - return "Invalid Value $aVal"; - } - } elsif ($aName =~ /^pin.*/) { - Log3 $name, 3, "$name: Invalid pin specification in attr $name $aName $aVal. Use something like pinD4 or PinA7"; - return "Invalid pin specification in attr $name $aName $aVal. Use something like pinD4 or PinA7"; + if ($aName =~ /^pin([DA]?\d+)/) { # pin attribute -> add a pin + my $pinName = $1; + return ArduCounter_ConfigurePin($hash, $pinName, $aVal); } elsif ($aName eq "devVerbose") { - if ($aVal =~ /^(\d+)\s*$/) { - my $t = $1; - if ($t > 100) { - Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal"; - return "Invalid Value $aVal"; - } - if ($hash->{Initialized}) { - ArduCounter_Write($hash, "${t}v"); - } else { - Log3 $name, 5, "$name: communication postponed until device is initialized"; - } - } else { - Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal"; - return "Invalid Value $aVal"; - } - + my $text = "devVerbose has been replaced by " . + 'enableHistory:0,1 ' . # history creation on device + 'enableSerialEcho:0,1,2 ' . # serial echo of output via TCP from device + 'enablePinDebug:0,1 ' . # show pin state changes from device + 'enableAnalogDebug:0,1,2,3 ' . # show analog levels + 'enableDevTime:0,1 ' . # device will send its time so drift can be detected + " please adapt you attribute configuration"; + + Log3 $name, 3, "$name: $text"; + return $text; + + } elsif ($aName eq "enableHistory") { + return ArduCounter_ConfigureVerboseLevels($hash, $aVal); + + } elsif ($aName eq "enableSerialEcho") { + return ArduCounter_ConfigureVerboseLevels($hash, undef, $aVal); + + } elsif ($aName eq "enablePinDebug") { + return ArduCounter_ConfigureVerboseLevels($hash, undef, undef, $aVal); + + } elsif ($aName eq "enableAnalogDebug") { + return ArduCounter_ConfigureVerboseLevels($hash, undef, undef, undef, $aVal); + + } elsif ($aName eq "enableDevTime") { + return ArduCounter_ConfigureVerboseLevels($hash, undef, undef, undef, undef, $aVal); + } elsif ($aName eq "analogThresholds") { + my $text = "analogThresholds has been removed. Thresholds are now part of the pin attribute. Please update your configuration"; + Log3 $name, 3, "$name: $text"; + if ($aVal =~ /^(\d+) (\d+)\s*$/) { my $min = $1; my $max = $2; @@ -812,28 +1112,13 @@ sub ArduCounter_Attr(@) return "Invalid Value $aVal"; } - } elsif ($aName eq "interval") { - if ($aVal =~ /^(\d+) (\d+) ?(\d+)? ?(\d+)?$/) { - my $min = $1; - my $max = $2; - my $sml = $3; - my $cnt = $4; - if ($min < 1 || $min > 3600 || $max < $min || $max > 3600) { - Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal"; - return "Invalid Value $aVal"; - } - if ($hash->{Initialized}) { - $sml = 0 if (!$sml); - $cnt = 0 if (!$cnt); - ArduCounter_Write($hash, "${min},${max},${sml},${cnt}i"); - } else { - Log3 $name, 5, "$name: communication postponed until device is initialized"; - } - } else { - Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal"; - return "Invalid Value $aVal"; - } - } elsif ($aName eq "factor") { + } elsif ($aName eq "interval") { + return ArduCounter_ConfigureIntervals($hash, $aVal); + + } elsif ($aName eq "board") { + $hash->{Board} = $aVal; + + } elsif ($aName eq "factor") { # log notice to remove this / replace if ($aVal =~ '^(\d+)$') { } else { Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal"; @@ -841,7 +1126,7 @@ sub ArduCounter_Attr(@) } } elsif ($aName eq "keepAliveDelay") { if ($aVal =~ '^(\d+)$') { - if ($aVal > 300) { + if ($aVal > 3600) { Log3 $name, 3, "$name: value too big in attr $name $aName $aVal"; return "Value too big: $aVal"; } @@ -859,57 +1144,51 @@ sub ArduCounter_Attr(@) Log3 $name, 3, "$name: disable attribute cleared"; ArduCounter_Open($hash) if ($init_done); # only if fhem is initialized } - } - - # handle wild card attributes -> Add to userattr to allow modification in fhemweb - #Log3 $name, 3, "$name: attribute $aName checking "; - if (" $modHash->{AttrList} " !~ m/ ${aName}[ :;]/) { - # nicht direkt in der Liste -> evt. wildcard attr in AttrList - foreach my $la (split / /, $modHash->{AttrList}) { - $la =~ /([^:;]+)(:?.*)/; - my $vgl = $1; # attribute name in list - probably a regex - my $opt = $2; # attribute hint in list - if ($aName =~ $vgl) { # yes - the name in the list now matches as regex - # $aName ist eine Ausprägung eines wildcard attrs - addToDevAttrList($name, "$aName" . $opt); # create userattr with hint to allow changing by click in fhemweb - if ($opt) { - # remove old entries without hint - my $ualist = $attr{$name}{userattr}; - $ualist = "" if(!$ualist); - my %uahash; - foreach my $a (split(/ /, $ualist)) { - if ($a !~ /^${aName}$/) { # entry in userattr list is attribute without hint - $uahash{$a} = 1; - } else { - Log3 $name, 3, "$name: added hint $opt to attr $a in userattr list"; - } - } - $attr{$name}{userattr} = join(" ", sort keys %uahash); - } - } + } elsif ($aName eq 'deviceDisplay') { + ArduCounter_ConfigureDisplay($hash, $aVal); + + } elsif ($aName =~ /pulsesPer/ || $aName =~ /[Ff]lowUnitTime/) { + my $now = gettimeofday(); + RemoveInternalTimer ("delayedcdisp:$name"); + InternalTimer($now, "ArduCounter_DelayedConfigureDisplay", "delayedcdisp:$name", 0); + + } elsif ($aName =~ /^verboseReadings([DA]?(\d+))/) { + my $arg = $1; + if (!$hash->{Initialized}) { # no hello received yet + return; # accept value for now. } - } else { - # exakt in Liste enthalten -> sicherstellen, dass keine +* etc. drin sind. - if ($aName =~ /\|\*\+\[/) { - Log3 $name, 3, "$name: Atribute $aName is not valid. It still contains wildcard symbols"; - return "$name: Atribute $aName is not valid. It still contains wildcard symbols"; + my ($pin, $pinName) = ArduCounter_ParsePin($hash, $arg); + return "illegal pin $arg" if (!defined($pin)); # parsePin logs error if wrong pin spec + + if ($aVal eq "0") { + ArduCounter_ReadingsDelete($hash, "lastMsg", $pin); + ArduCounter_ReadingsDelete($hash, "pinHistory", $pin); + } elsif ($aVal eq "-1") { + readingsDelete($hash, 'pin' . $pin); + readingsDelete($hash, 'pin' . $pinName); + readingsDelete($hash, 'long' . $pin); + readingsDelete($hash, 'long' . $pinName); + readingsDelete($hash, 'countDiff' . $pin); + readingsDelete($hash, 'countDiff' . $pinName); + readingsDelete($hash, 'timeDiff' . $pin); + readingsDelete($hash, 'timeDiff' . $pinName); + readingsDelete($hash, 'reject' . $pin); + readingsDelete($hash, 'reject' . $pinName); + readingsDelete($hash, 'interpolatedLong' . $pin); + readingsDelete($hash, 'interpolatedLong' . $pinName); } - } + } + ArduCounter_ManageUserAttr($hash, $aName); } elsif ($cmd eq "del") { - if ($aName =~ 'pin.*') { - if ($aName !~ 'pin([aAdD]?\d+)') { - Log3 $name, 3, "$name: Invalid pin name in attr $name $aName $aVal"; - return "Invalid pin name $aName"; + if ($aName =~ 'pin(.*)') { + my $arg = $1; + if (!$hash->{Initialized}) { # no hello received yet + return; # accept value for now. } - my $pin = $1; - # todo: convert to internal value with AnalogPinMap - - if ($hash->{Initialized}) { # did device already report its version? + my ($pin, $pinName) = ArduCounter_ParsePin($hash, $arg); + if (defined($pin)) { ArduCounter_Write( $hash, "${pin}d"); - } else { - Log3 $name, 5, "$name: pin config can not be deleted since device is not initialized yet"; - return "device is not initialized yet"; } } elsif ($aName eq 'disable') { @@ -921,9 +1200,10 @@ sub ArduCounter_Attr(@) } -# SET command + ######################################################################### -sub ArduCounter_Flash($$) +# flash a device via serial or OTA with external commands +sub ArduCounter_Flash($@) { my ($hash, @args) = @_; my $name = $hash->{NAME}; @@ -932,23 +1212,74 @@ sub ArduCounter_Flash($$) my $port = $deviceName[0]; my $firmwareFolder = "./FHEM/firmware/"; my $logFile = AttrVal("global", "logdir", "./log") . "/ArduCounterFlash.log"; + my $ip; + + if ($port =~ /(\d+\.\d+\.\d+\.\d+):(\d+)/) { + $ip = $1; + $port = $1; + } - return "Flashing ESP8266 not supported yet" if ($hash->{Board} =~ /ESP8266/); + my $hexFile = shift @args; + my $netPort = 0; - my $hexFile = $firmwareFolder . "ArduCounter.hex"; + my $flashCommand = AttrVal($name, "flashCommand", ""); + if ($hash->{Board} =~ /ESP8266/ ) { + $hexFile = "ArduCounter-ESP8266.bin" if (!$hexFile); + $netPort = 8266; + if (!$flashCommand ) { + if ($hash->{TCP}) { + $flashCommand = 'espota.py -i[IP] -p [NETPORT] -f [BINFILE] >[LOGFILE] 2>&1'; + } else { + $flashCommand = 'esptool.py --chip esp8266 --port [PORT] --baud 115200 write_flash 0x0 [BINFILE] >[LOGFILE] 2>&1'; + } + } + } elsif ($hash->{Board} =~ /ESP32/ || $hash->{Board} =~ /T-Display/ ) { + $netPort = 3232; + if ($hash->{Board} =~ /T-Display/ ) { + $hexFile = "ArduCounter-ESP32T.bin" if (!$hexFile); + } else { + $hexFile = "ArduCounter-ESP32.bin" if (!$hexFile); + } + if (!$flashCommand ) { + if ($hash->{TCP}) { + $flashCommand = 'espota.py -i[IP] -p [NETPORT] -f [BINFILE] 2>[LOGFILE]'; + } else { + $flashCommand = 'esptool.py --chip esp32 --port [PORT] --baud 460800 --before default_reset --after hard_reset write_flash -z ' . + '--flash_mode dio --flash_freq 40m --flash_size detect ' . + '0x1000 FHEM/firmware/ArduCounter-ESP32-bootloader_dio_40m.bin ' . + '0x8000 FHEM/firmware/ArduCounter-ESP32-partitions.bin ' . + '0xe000 FHEM/firmware/ArduCounter-ESP32-boot_app0.bin ' . + '0x10000 [BINFILE] >[LOGFILE] 2>&1'; + # to install do apt-get install python3, python3-pip + # and then pip3 install esptool + } + } + } elsif ($hash->{Board} =~ /NANO/ ) { + $hexFile = "ArduCounter-NANO.hex" if (!$hexFile); + $flashCommand = 'avrdude -p atmega328P -b 57600 -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]' if (!$flashCommand); + } elsif ($hash->{Board} =~ /UNO/ ) { + $hexFile = "ArduCounter-NANO.hex" if (!$hexFile); + $flashCommand = 'avrdude -p atmega328P -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]' if (!$flashCommand); + } else { + if (!$hash->{Board}) { + return "Flashing not possible if board type is unknown and no filename given. Try setting the board attribute (ESP8266, ESP32 or NANO)."; + } else { + return "Flashing $hash->{Board} not supported or board attribute wrong (should be ESP8266, ESP32 or NANO)"; + } + } + + $hexFile = $firmwareFolder . $hexFile; return "The file '$hexFile' does not exist" if(!-e $hexFile); - Log3 $name, 3, "$name: Flashing Aduino at $port with $hexFile. See $logFile for details"; + Log3 $name, 3, "$name: Flashing device at $port with $hexFile. See $logFile for details"; $log .= "flashing device as ArduCounter for $name\n"; - $log .= "hex file: $hexFile\n"; + $log .= "firmware file: $hexFile\n"; $log .= "port: $port\n"; $log .= "log file: $logFile\n"; - my $flashCommand = AttrVal($name, "flashCommand", ""); - - if($flashCommand ne "") { + if($flashCommand) { if (-e $logFile) { unlink $logFile; } @@ -957,36 +1288,101 @@ sub ArduCounter_Flash($$) DevIo_CloseDev($hash); $log .= "$name closed\n"; - my $avrdude = $flashCommand; - $avrdude =~ s/\Q[PORT]\E/$port/g; - $avrdude =~ s/\Q[HEXFILE]\E/$hexFile/g; - $avrdude =~ s/\Q[LOGFILE]\E/$logFile/g; + $flashCommand =~ s/\Q[PORT]\E/$port/g; + $flashCommand =~ s/\Q[IP]\E/$ip/g; + $flashCommand =~ s/\Q[HEXFILE]\E/$hexFile/g; + $flashCommand =~ s/\Q[BINFILE]\E/$hexFile/g; + $flashCommand =~ s/\Q[LOGFILE]\E/$logFile/g; + $flashCommand =~ s/\Q[NETPORT]\E/$netPort/g; - $log .= "command: $avrdude\n\n"; - `$avrdude`; + $log .= "command: $flashCommand\n\n"; + `$flashCommand`; local $/=undef; if (-e $logFile) { open FILE, $logFile; my $logText = ; close FILE; - $log .= "--- AVRDUDE ---------------------------------------------------------------------------------\n"; + $log .= "--- flash command ---------------------------------------------------------------------------------\n"; $log .= $logText; - $log .= "--- AVRDUDE ---------------------------------------------------------------------------------\n\n"; + $log .= "--- flash command ---------------------------------------------------------------------------------\n\n"; } else { - $log .= "WARNING: avrdude created no log file\n\n"; + $log .= "WARNING: flash command created no log file\n\n"; } - ArduCounter_Open($hash, 0); # new open - $log .= "$name open called.\n"; delete $hash->{Initialized}; + my $now = gettimeofday(); + my $delay = 5; # wait 5 seconds to give device time for reboot + Log3 $name, 4, "$name: ArduCounter_Flash set internal timer to call open"; + RemoveInternalTimer ("delayedopen:$name"); + InternalTimer($now+$delay, "ArduCounter_DelayedOpen", "delayedopen:$name", 0); + $log .= "$name internal timer set to call open.\n"; + } else { + return "Flashing not possible if flash command is not set for this board and connection"; } return $log; } -# SET command + +##################################################### +# parse pin input and return pin number and pin name +sub ArduCounter_ParsePin($$) +{ + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + if ($arg !~ /^([DA]?)(\d+)/) { + Log3 $name, 3, "$name: parseTime got invalid pin spec $arg"; + return undef; + } + my $pinType = $1; + my $pin = $2; + $pin = ArduCounter_PinNumber($hash, $pinType.$pin) if ($pinType eq 'A'); + my $pinName = ArduCounter_PinName ($hash, $pin); + + # if board did send allowed pins, check here + if ($hash->{allowedPins}) { # list of allowed pins received with hello + my %pins = map { $_ => 1 } split (/,/, $hash->{allowedPins}); + if ($init_done && %pins && !$pins{$pin}) { + Log3 $name, 3, "$name: Invalid / disallowed pin in specification $arg"; + return undef; + } + } + return ($pin, $pinName); +} + + ######################################################################### +# clears all counter readings for a specified pin number +# called from set with a pin number +sub ArduCounter_ClearPinCounters($$) +{ + my ($hash, $pin) = @_; + ArduCounter_Write($hash, "${pin}c"); + my ($err, $msg) = ArduCounter_ReadAnswer($hash, '(cleared \d+)|(Error:)'); + + ArduCounter_ReadingsDelete($hash, 'pin', $pin); # internal device counter pinX + ArduCounter_ReadingsDelete($hash, 'long', $pin); # long counter longX + ArduCounter_ReadingsDelete($hash, 'interpolated', $pin); # interpolated long counter interpolatedLongX + ArduCounter_ReadingsDelete($hash, 'calcCounter', $pin); # calculated counter calcCounterX + ArduCounter_ReadingsDelete($hash, 'calcCounter_i', $pin); # calculated counter - ignored units calcCounterX_i + ArduCounter_ReadingsDelete($hash, 'power', $pin); # power reading powerX + ArduCounter_ReadingsDelete($hash, 'seq', $pin); # sequence seqX + + ArduCounter_ReadingsDelete($hash, 'reject', $pin); # rejected pulsesin last reportng period rejectX + ArduCounter_ReadingsDelete($hash, 'timeDiff', $pin); # time difference of last reporting period timeDiffX + ArduCounter_ReadingsDelete($hash, 'countDiff', $pin); # count difference of last reporting period countDiffX + ArduCounter_ReadingsDelete($hash, 'lastMsg', $pin); # last message from device + + ArduCounter_ReadingsDelete($hash, "runTime", $pin); + ArduCounter_ReadingsDelete($hash, "runTimeIgnore", $pin); + ArduCounter_ReadingsDelete($hash, ".switchOnTime", $pin); + ArduCounter_ReadingsDelete($hash, ".lastCheckIgnoreTime", $pin); +} + + +######################################################################### +# SET command sub ArduCounter_Set($@) { my ($hash, @a) = @_; @@ -1015,6 +1411,7 @@ sub ArduCounter_Set($@) } elsif ($attr eq "reconnect") { Log3 $name, 4, "$name: set reconnect called"; DevIo_CloseDev($hash); + delete $hash->{OpenRetries}; ArduCounter_Open($hash); return; @@ -1022,13 +1419,41 @@ sub ArduCounter_Set($@) delete $hash->{analogLevels}; return; + } elsif ($attr eq "clearHistory") { + # remove history + delete $hash->{History}; + delete $hash->{HistoryPin}; + delete $hash->{LastHistSeq}; + delete $hash->{HistIdx}; + return; + + } elsif ($attr eq "clearCounters") { # clear counters for a specific pin + my ($pin, $pinName) = ArduCounter_ParsePin($hash, $arg); + return "illegal pin $arg" if (!defined($pin)); # parsePin logs error if wrong pin spec + + Log3 $name, 4, "$name: Set $attr $arg called - removing all readings for pin $pinName, internal $pin"; + ArduCounter_ClearPinCounters($hash, $pin); + + } elsif ($attr eq "counter") { # set counters for a specific pin + if ($arg =~ /([AD]?\d+)[\s\,]+([\d\.]+)/) { + my $val = $2; + my ($pin, $pinName) = ArduCounter_ParsePin($hash, $1); + return "illegal pin $arg" if (!defined($pin)); # parsePin logs error if wrong pin spec + Log3 $name, 4, "$name: Set $attr $arg called - setting calcCounter for pin $pinName"; + readingsBeginUpdate($hash); + ArduCounter_BulkUpdate($hash, 'calcCounter', $pin, $val); + readingsEndUpdate($hash,1); + } else { + return "wrong syntax, use set counters pin value"; + } + } elsif ($attr eq "flash") { return ArduCounter_Flash($hash, @a); } if(!$hash->{FD}) { Log3 $name, 4, "$name: Set $attr $arg called but device is disconnected"; - return ("Set called but device is disconnected", undef); + return "Set called but device is disconnected"; } if (IsDisabled($name)) { Log3 $name, 4, "$name: set $attr $arg called but device is disabled"; @@ -1045,12 +1470,18 @@ sub ArduCounter_Set($@) } elsif ($attr eq "reset") { Log3 $name, 4, "$name: set reset called"; - DevIo_CloseDev($hash); - ArduCounter_Open($hash); if (ArduCounter_Write($hash, "r")) { delete $hash->{Initialized}; - return "sent (r)eset command to device - waiting for its setup message"; } + DevIo_CloseDev($hash); + ArduCounter_Open($hash); + return "sent (r)eset command to device - waiting for its setup message"; + + } elsif ($attr eq "resetWifi") { + Log3 $name, 4, "$name: set resetWifi called"; + ArduCounter_Write($hash, "w"); + return "sent (w) command to device"; + } return undef; } @@ -1112,33 +1543,49 @@ sub ArduCounter_Get($@) $count++; $idx = 0 if ($idx > AttrVal($name, "maxHist", 1000)); } - if (!$hash->{runningCfg}{V} || $hash->{runningCfg}{V} < 5) { - $ret = "Make sure that devVerbose is set to 5 or higher to get pin history data\n" . - "current devVerbose at device is " . ($hash->{runningCfg}{V} ? $hash->{runningCfg}{V} : "undef") . "\n\n" . - $ret; + if (!AttrVal($name, "enableHistory", 0)) { + $ret = "Make sure that enableHistory is set to 1 to get pin history data\n" . $ret; } return ($ret ? $ret : "no history data so far"); } - return undef; } -###################################### -sub ArduCounter_HandleDeviceTime($$$$) +########################################### +# calculate and log drift of device time +# called from parse_hello with T and B line, +# from parse with new N line +# and parse report with only N +sub ArduCounter_ParseTime($$$) { - my ($hash, $deTi, $deTiW, $now) = @_; + my ($hash, $line, $now) = @_; my $name = $hash->{NAME}; - my $deviceNowSecs = ($deTi/1000) + ((0xFFFFFFFF / 1000) * $deTiW); - Log3 $name, 5, "$name: Device Time $deviceNowSecs"; + if ($line !~ /^[NT](\d+),(\d+) *(?:B(\d+),(\d+))?/) { + Log3 $name, 4, "$name: probably wrong firmware version - cannot parse line $line"; + return; + } + my $mNow = $1; + my $mNowW = $2; + my $mBoot = $3; + my $mBootW = $4; + + my $deviceNowSecs = ($mNow/1000) + ((0xFFFFFFFF / 1000) * $mNowW); + #Log3 $name, 5, "$name: Device Time $deviceNowSecs"; + if (defined($mBoot)) { + my $deviceBootSecs = ($mBoot/1000) + ((0xFFFFFFFF / 1000) * $mBootW); + my $bootTime = $now - ($deviceNowSecs - $deviceBootSecs); + $hash->{deviceBooted} = $bootTime; # for estimation of missed pulses up to now + } if (defined ($hash->{'.DeTOff'}) && $hash->{'.LastDeT'}) { if ($deviceNowSecs >= $hash->{'.LastDeT'}) { $hash->{'.Drift2'} = ($now - $hash->{'.DeTOff'}) - $deviceNowSecs; } else { $hash->{'.DeTOff'} = $now - $deviceNowSecs; - Log3 $name, 4, "$name: device did reset (now $deviceNowSecs, before $hash->{'.LastDeT'}). New offset is $hash->{'.DeTOff'}"; + Log3 $name, 4, "$name: device did reset (now $deviceNowSecs, before $hash->{'.LastDeT'})." . + " New offset is $hash->{'.DeTOff'}"; } } else { $hash->{'.DeTOff'} = $now - $deviceNowSecs; @@ -1149,185 +1596,242 @@ sub ArduCounter_HandleDeviceTime($$$$) $hash->{'.LastDeT'} = $deviceNowSecs; my $drTime = ($now - $hash->{'.DriftStart'}); - #Log3 $name, 5, "$name: Device Time $deviceNowSecs" . - #", Offset " . sprintf("%.3f", $hash->{'.DeTOff'}/1000) . - ", Drift " . sprintf("%.3f", $hash->{'.Drift2'}) . - "s in " . sprintf("%.3f", $drTime) . "s" . - ($drTime > 0 ? ", " . sprintf("%.2f", $hash->{'.Drift2'} / $drTime * 100) . "%" : ""); + Log3 $name, 5, "$name: Device Time $deviceNowSecs" . + ", Offset " . sprintf("%.3f", $hash->{'.DeTOff'}/1000) . + ", Drift " . sprintf("%.3f", $hash->{'.Drift2'}) . + "s in " . sprintf("%.3f", $drTime) . "s" . + ($drTime > 0 ? ", " . sprintf("%.2f", $hash->{'.Drift2'} / $drTime * 100) . "%" : ""); } - -###################################### + +sub ArduCounter_ParseAvailablePins($$) { + my ($hash, $line) = @_; + my $name = $hash->{NAME}; + + # now enrich $line with $rAnalogPinMap{$hash->{Board}}{$pin} + if ($line && $hash->{Board}) { + my $newAllowed; + my $first = 1; + foreach my $pin (split (/,/, $line)) { + $newAllowed .= ($first ? '' : ','); # separate by , if not empty anymore + $newAllowed .= $pin; + if ($rAnalogPinMap{$hash->{Board}}{$pin}) { + $newAllowed .= ",$rAnalogPinMap{$hash->{Board}}{$pin}"; + } + $first = 0; + } + $hash->{allowedPins} = $newAllowed; + } +} + + +#################################################### +# Hello is sent after reconnect or restart +# check firmware version, set device boot time hash +# set timer to configure device sub ArduCounter_ParseHello($$$) { my ($hash, $line, $now) = @_; my $name = $hash->{NAME}; - if ($line =~ /^ArduCounter V([\d\.]+) on ([^\ ]+)( ?[^\ ]*) compiled (.*) Hello(, pins ([0-9\,]+) available)? ?(T([\d]+),([\d]+) B([\d]+),([\d]+))?/) { # setup / hello message - $hash->{VersionFirmware} = ($1 ? $1 : 'unknown'); - $hash->{Board} = ($2 ? $2 : 'unknown'); - $hash->{BoardDet} = ($3 ? $3 : ''); - $hash->{SketchCompile} = ($4 ? $4 : 'unknown'); - $hash->{allowedPins} = $6 if ($6); - my $mNow = ($8 ? $8 : 0); - my $mNowW = ($9 ? $9 : 0); - my $mBoot = ($10 ? $10 : 0); - my $mBootW = ($11 ? $11 : 0); - if ($hash->{VersionFirmware} < "2.36") { - $hash->{VersionFirmware} .= " - not compatible with this Module version - please flash new sketch"; - Log3 $name, 3, "$name: device reported outdated Arducounter Firmware ($hash->{VersionFirmware}) - please update!"; - delete $hash->{Initialized}; + if ($line !~ /^ArduCounter V([\d\.]+) on ([^\ ]+) ?(.*) compiled (.*) (?:Started|Hello)(, pins ([0-9\,]+) available)? ?(T(\d+),(\d+) B(\d+),(\d+))?/) { + Log3 $name, 4, "$name: probably wrong firmware version - cannot parse line $line"; + return; + } + $hash->{VersionFirmware} = ($1 ? $1 : ''); + $hash->{Board} = ($2 ? $2 : 'unknown'); + $hash->{BoardDet} = ($3 ? $3 : ''); + $hash->{SketchCompile} = ($4 ? $4 : 'unknown'); + my $allowedPins = $6; + my $dTime = $7; + + my $boardAttr = AttrVal($name, 'board', ''); + if ($hash->{Board} && $boardAttr && ($hash->{Board} ne $boardAttr)) { + Log3 $name, 3, "attribute board is set to $boardAttr and is overwriting board $hash->{Board} reported by device"; + $hash->{Board} = $boardAttr; + } + + if (!$hash->{VersionFirmware} || $hash->{VersionFirmware} < "2.36") { + $hash->{VersionFirmware} .= " - not compatible with this Module version - please flash new sketch"; + Log3 $name, 3, "$name: device reported outdated Arducounter Firmware ($hash->{VersionFirmware}) - please update!"; + delete $hash->{Initialized}; + } else { + if ($hash->{VersionFirmware} < "4.00") { + Log3 $name, 3, "$name: device sent hello with outdated Arducounter Firmware ($hash->{VersionFirmware}) - please update!"; } else { Log3 $name, 3, "$name: device sent hello: $line"; - $hash->{Initialized} = 1; # now device has finished its boot and reported its version - delete $hash->{runningCfg}; - - my $cft = AttrVal($name, "configDelay", 1); # wait for device to send cfg before reconf. - RemoveInternalTimer ("cmpCfg:$name"); - InternalTimer($now+$cft, "ArduCounter_ConfigureDevice", "cmpCfg:$name", 0); - - my $deviceNowSecs = ($mNow/1000) + ((0xFFFFFFFF / 1000) * $mNowW); - my $deviceBootSecs = ($mBoot/1000) + ((0xFFFFFFFF / 1000) * $mBootW); - my $bootTime = $now - ($deviceNowSecs - $deviceBootSecs); - $hash->{deviceBooted} = $bootTime; # for estimation of missed pulses up to now - - my $boardAttr = AttrVal($name, 'board', ''); - if ($hash->{Board} && $boardAttr && ($hash->{Board} ne $boardAttr)) { - Log3 $name, 3, "attribute board is set to $boardAttr and is overwriting board $hash->{Board} reported by device"; - $hash->{Board} = $boardAttr; - } - # now enrich $hash->{allowedPins} with $rAnalogPinMap{$hash->{Board}}{$pin} - if ($hash->{allowedPins} && $hash->{Board}) { - my $newAllowed; - my $first = 1; - foreach my $pin (split (/,/, $hash->{allowedPins})) { - $newAllowed .= ($first ? '' : ','); # separate by , if not empty anymore - $newAllowed .= $pin; - if ($rAnalogPinMap{$hash->{Board}}{$pin}) { - $newAllowed .= ",$rAnalogPinMap{$hash->{Board}}{$pin}"; - } - $first = 0; - } - $hash->{allowedPins} = $newAllowed; - } } - delete $hash->{WaitForHello}; - RemoveInternalTimer ("hwait:$name"); # dont wait for hello reply if already sent - RemoveInternalTimer ("sendHello:$name"); # Hello not needed anymore if not sent yet - } else { - Log3 $name, 4, "$name: probably wrong firmware version - cannot parse line $line"; + $hash->{Initialized} = 1; # device has finished its boot and reported version + + my $cft = AttrVal($name, "configDelay", 1); # wait for device to send cfg before reconf. + RemoveInternalTimer ("cmpCfg:$name"); + InternalTimer($now+$cft, "ArduCounter_ConfigureDevice", "cmpCfg:$name", 0); + + ArduCounter_ParseTime($hash, $dTime, $now) if ($dTime); + ArduCounter_ParseAvailablePins($hash, $allowedPins) if ($allowedPins); } + delete $hash->{runningCfg}; # new config will be sent now + delete $hash->{WaitForHello}; + delete $hash->{OpenRetries}; + + # remove old history - sequences won't fit anymore for future history messages + delete $hash->{History}; + delete $hash->{HistoryPin}; + delete $hash->{LastHistSeq}; + delete $hash->{HistIdx}; + + RemoveInternalTimer ("hwait:$name"); # dont wait for hello reply if already sent + RemoveInternalTimer ("sendHello:$name"); # Hello not needed anymore if not sent yet } -###################################### -# $hash->{Board} wird in parseHello gesetzt und ggf. dort gleich durch das Attribut Board überschrieben -# called from Attr and ConfigureDevice. -# in all cases Board and AllowedPins have been received with hello before -sub ArduCounter_PinNumber($$) + +######################################################################################### +# return the name of the reading for a passed internal name +# like 'long' or 'calcCounter' and its pin Number +# depending on verboseReadings and readingName attributes +# called with a base name and a pin number +sub ArduCounter_ReadingName($$$) { - my ($hash, $pinName) = @_; - my $name = $hash->{NAME}; - my $boardAttr = AttrVal($name, "board", ""); - my $board = ($boardAttr ? $boardAttr : $hash->{Board}); - my $pin; - - if (!$board) { # maybe no hello received yet and no Board-attr set (should never be the case) - my @boardOptions = keys %AnalogPinMap; - my $count = 0; - foreach my $candidate (@boardOptions) { - if ($AnalogPinMap{$candidate}{$pinName}) { - $board = $AnalogPinMap{$candidate}{$pinName}; - $count++; - } - } - if ($count > 1) { - Log3 $name, 3, "$name: PinNumber called from " . ArduCounter_Caller() . " can not determine internal pin number for $pinName, board type is not known (yet) and attribute Board is also not set"; - } elsif (!$count) { - Log3 $name, 3, "$name: PinNumber called from " . ArduCounter_Caller() . " can not determine internal pin number for $pinName. No known board seems to support it"; - } - } - $pin = $AnalogPinMap{$board}{$pinName} if ($board); - if ($pin) { - Log3 $name, 5, "$name: PinNumber called from " . ArduCounter_Caller() . " returns $pin for $pinName"; - } else { - Log3 $name, 5, "$name: PinNumber called from " . ArduCounter_Caller() . " returns unknown for $pinName"; - } - return $pin # might be undef -} - - -###################################### -sub ArduCounter_PinName($$) -{ - my ($hash, $pin) = @_; - my $name = $hash->{NAME}; - - my $pinName = $pin; # start assuming that attrs are set as pinX - if (!AttrVal($name, "pin$pinName", 0)) { # if not - if (AttrVal($name, "pinD$pin", 0)) { - $pinName = "D$pin"; # maybe pinDX? - #Log3 $name, 5, "$name: using attrs with pin name D$pin"; - } elsif ($hash->{Board}) { - my $aPin = $rAnalogPinMap{$hash->{Board}}{$pin}; - if ($aPin) { # or pinAX? - $pinName = "$aPin"; - #Log3 $name, 5, "$name: using attrs with pin name $pinName instead of $pin or D$pin (Board $hash->{Board})"; - } - } - } - return $pinName; -} - - -sub AduCounter_AttrVal($$$;$$$) -{ - my ($hash, $default, $a1, $a2, $a3, $a4) = @_; - my $name = $hash->{NAME}; - return AttrVal($name, $a1, undef) if (defined (AttrVal($name, $a1, undef))); - return AttrVal($name, $a2, undef) if (defined ($a2) && defined (AttrVal($name, $a2, undef))); - return AttrVal($name, $a3, undef) if (defined ($a3) && defined (AttrVal($name, $a3, undef))); - return AttrVal($name, $a4, undef) if (defined ($a4) && defined (AttrVal($name, $a4, undef))); - return $default; -} - - -###################################### -sub ArduCounter_LogPinDesc($$) -{ - my ($hash, $pin) = @_; + my ($hash, $rBaseName, $pin) = @_; + my $name = $hash->{NAME}; my $pinName = ArduCounter_PinName ($hash, $pin); - return AduCounter_AttrVal($hash, "pin$pin", "readingNameCount$pinName", "readingNameCount$pin", "readingNamePower$pinName", "readingNamePower$pin"); + my $verbose = ArduCounter_AttrVal($hash, 0, "verboseReadings$pinName", "verboseReadings$pin"); + if ($verbose !~ /\-?[0-9]+/) { + Log3 $name, 3, "illegal setting for verboseReadings: $verbose"; + $verbose = 0; + } + if ($rBaseName eq 'pin') { + my $default = ($verbose >= 0 ? "pin$pinName" : ".pin$pinName"); # hidden if verboseReadings < 0 + return ArduCounter_AttrVal($hash, $default, "readingNameCount$pinName", "readingNameCount$pin"); + } elsif ($rBaseName eq 'long') { + my $default = ($verbose >= 0 ? "long$pinName" : ".long$pinName"); # hidden if verboseReadings < 0 + return ArduCounter_AttrVal($hash, $default, "readingNameLongCount$pinName", "readingNameLongCount$pin"); + } elsif ($rBaseName eq 'interpolated') { + my $default = ($verbose >= 0 ? "interpolatedLong$pinName" : ""); # no reading if verboseReadings < 0 + return ArduCounter_AttrVal($hash, $default, "readingNameInterpolatedCount$pinName", "readingNameInterpolatedCount$pin"); + } elsif ($rBaseName eq 'calcCounter') { + return ArduCounter_AttrVal($hash, "calcCounter$pinName", "readingNameCalcCount$pinName", "readingNameCalcCount$pin"); + } elsif ($rBaseName eq 'calcCounter_i') { + return ArduCounter_AttrVal($hash, "calcCounter$pinName" . "_i", "readingNameCalcCount$pinName" . "_i", "readingNameCalcCount$pin" . "_i"); + } elsif ($rBaseName eq 'timeDiff') { + return ($verbose >= 0 ? "timeDiff$pinName" : ".timeDiff$pinName"); # hidden if verboseReadings < 0 + } elsif ($rBaseName eq 'countDiff') { + return ($verbose >= 0 ? "countDiff$pinName" : ".countDiff$pinName"); # hidden if verboseReadings < 0 + } elsif ($rBaseName eq 'reject') { + return ($verbose >= 0 ? "reject$pinName" : ".reject$pinName"); # hidden if verboseReadings < 0 + } elsif ($rBaseName eq 'power') { + return ArduCounter_AttrVal($hash, "power$pinName", "readingNamePower$pinName", "readingNamePower$pin"); + } elsif ($rBaseName eq 'lastMsg') { + return ($verbose > 0 ? "lastMsg$pinName" : ""); # no reading if verboseReadings < 1 + } elsif ($rBaseName eq 'pinHistory') { + return ($verbose > 0 ? "pinHistory$pinName" : ""); # no reading if verboseReadings < 1 + } elsif ($rBaseName eq 'seq') { + return ($verbose > 0 ? ".seq$pinName" : ""); # always hidden + } else { + return $rBaseName . $pinName; + } } ######################################################################### -sub ArduCounter_HandleCounters($$$$$$$$) +# return the value of the reading +# with a passed internal name and a pin number +# depending on verboseReadings and readingName attributes +sub ArduCounter_ReadingsVal($$$;$) { - my ($hash, $pin, $seq, $count, $time, $diff, $rDiff, $now) = @_; + my ($name, $rBaseName, $pin, $default) = @_; + my $hash = $defs{$name}; + + $default = 0 if (!defined($default)); + my $rName = ArduCounter_ReadingName($hash, $rBaseName, $pin); + return $default if (!$rBaseName); + return ReadingsVal($name, $rName, $default); +} + + +######################################################################### +# return the value of the reading +# depending on verboseReadings and readingName attributes +# only called from HandleCounters with a base name and a pin number +sub ArduCounter_ReadingsTimestamp($$$) +{ + my ($name, $rBaseName, $pin) = @_; + my $hash = $defs{$name}; + + my $rName = ArduCounter_ReadingName($hash, $rBaseName, $pin); + return 0 if (!$rBaseName); + return ReadingsTimestamp($name, $rName, 0); +} + + +######################################################################### +# bulk update readings +# depending on verboseReadings and readingName attributes +# called from functions that handle device reports +# with a base name and a pin number, value and optional time +sub ArduCounter_BulkUpdate($$$$;$) +{ + my ($hash, $rBaseName, $pin, $value, $sTime) = @_; + my $name = $hash->{NAME}; + my $rName = ArduCounter_ReadingName($hash, $rBaseName, $pin); + if (!$rName) { + #Log3 $name, 5, "BulkUpdate - suppress reading $rBaseName for pin $pin"; + return; + } + if (defined($sTime)) { + my $fSdTim = FmtTime($sTime); # only time formatted for logging + my $fSTime = FmtDateTime($sTime); # date time formatted for reading + Log3 $name, 5, "ReadingsUpdate - readingStartTime specified: setting timestamp to $fSdTim"; + my $chIdx = 0; + $hash->{".updateTime"} = $sTime; + $hash->{".updateTimestamp"} = $fSTime; + readingsBulkUpdate($hash, $rName, $value); + $hash->{CHANGETIME}[$chIdx++] = $fSTime; # Intervall start + readingsEndUpdate($hash, 1); # end of special block + readingsBeginUpdate($hash); # start regular update block + } else { + readingsBulkUpdate($hash, $rName, $value); + } + return; +} + + +######################################################################### +# delete readings +# depending on verboseReadings and readingName attributes +# called with a pin number +sub ArduCounter_ReadingsDelete($$$) +{ + my ($hash, $rBaseName, $pin) = @_; + my $name = $hash->{NAME}; + + my $rName = ArduCounter_ReadingName($hash, $rBaseName, $pin); + readingsDelete($hash, $rName); + return; +} + + +######################################################################### +sub ArduCounter_HandleCounters($$$$$$$$$) +{ + my ($hash, $pin, $seq, $count, $time, $diff, $rDiff, $now, $ppu) = @_; my $name = $hash->{NAME}; my $pinName = ArduCounter_PinName ($hash, $pin); - - my $rcname = AduCounter_AttrVal($hash, "pin$pinName", "readingNameCount$pinName", "readingNameCount$pin"); - my $rlname = AduCounter_AttrVal($hash, "long$pinName", "readingNameLongCount$pinName", "readingNameLongCount$pin"); - my $riname = AduCounter_AttrVal($hash, "interpolatedLong$pinName", "readingNameInterpolatedCount$pinName", "readingNameInterpolatedCount$pin"); - my $rccname = AduCounter_AttrVal($hash, "calcCounter$pinName", "readingNameCalcCount$pinName", "readingNameCalcCount$pin"); - my $ppk = AduCounter_AttrVal($hash, 0, "readingPulsesPerKWh$pin", "readingPulsesPerKWh$pinName", "pulsesPerKWh"); my $lName = ArduCounter_LogPinDesc($hash, $pin); - - my $longCount = ReadingsVal($name, $rlname, 0); # alter long count Wert - my $intpCount = ReadingsVal($name, $riname, 0); # alter interpolated count Wert - my $lastCount = ReadingsVal($name, $rcname, 0); - my $cCounter = ReadingsVal($name, $rccname, 0); # calculated counter - my $iSum = ReadingsVal($name, $rccname . "_i", 0); # interpolation sum - my $lastSeq = ReadingsVal($name, "seq".$pinName, 0); - my $intrCount = 0; # interpolated count to be added - - my $lastCountTS = ReadingsTimestamp ($name, $rlname, 0); # last time long count reading was set as string + my $pLog = "$name: pin $pinName ($lName)"; # to be used as start of log lines + + my $longCount = ArduCounter_ReadingsVal($name, 'long', $pin); # alter long count Wert + my $intpCount = ArduCounter_ReadingsVal($name, 'interpolated', $pin); # alter interpolated count Wert + my $lastCount = ArduCounter_ReadingsVal($name, 'pin', $pin); + my $cCounter = ArduCounter_ReadingsVal($name, 'calcCounter', $pin); # calculated counter + my $iSum = ArduCounter_ReadingsVal($name, 'calcCounter_i', $pin); # ignored sum + my $lastSeq = ArduCounter_ReadingsVal($name, 'seq', $pin); + my $intrCount = 0; # interpolated count to be added + + my $lastCountTS = ArduCounter_ReadingsTimestamp ($name, 'pin', $pin); # last time long count reading was set as string my $lastCountTNum = time_str2num($lastCountTS); # time as number - my $fLastCTim = FmtTime($lastCountTNum); # formatted for logging - my $pLog = "$name: pin $pinName ($lName)"; # start of log lines my $fBootTim; my $deviceBooted; @@ -1344,6 +1848,7 @@ sub ArduCounter_HandleCounters($$$$$$$$) my $seqGap = $seq - ($lastSeq + 1); # gap of reporting sequences if any $seqGap = 0 if (!$lastCountTS); # readings didn't exist yet + if ($seqGap < 0) { # new sequence number is smaller than last $seqGap %= 256; # correct seq gap Log3 $name, 5, "$pLog sequence wrapped from $lastSeq to $seq, set seqGap to $seqGap" if (!$deviceBooted); @@ -1351,26 +1856,19 @@ sub ArduCounter_HandleCounters($$$$$$$$) my $pulseGap = $countStart - $lastCount; # gap of missed pulses if any $pulseGap = 0 if (!$lastCountTS); # readings didn't exist yet - if ($pulseGap < 0) { # pulseGap < 0 should not happen - $pulseGap = 0; - Log3 $name, 3, "$pLog seems to have missed $seqGap reports in $timeGap seconds. " . - "Last reported sequence was $lastSeq, now $seq. " . - "Device count before was $lastCount, now $count with rDiff $rDiff " . - "but pulseGap is $pulseGap. this is probably wrong and should not happen" if (!$deviceBooted); - } if ($deviceBooted) { # first report for this pin after a restart -> do interpolation # interpolate for period between last report before boot and boot time. - Log3 $name, 5, "$pLog device restarted at $fBootTim, last reported at $fLastCTim, " . + Log3 $name, 5, "$pLog device restarted at $fBootTim, last reported at " . FmtTime($lastCountTNum) . " " . "count changed from $lastCount to $count, sequence from $lastSeq to $seq"; $seqGap = $seq - 1; # $seq should be 1 after restart $pulseGap = $countStart; # we missed everything up to the count at start of the reported interval - my $lastInterval = ReadingsVal ($name, "timeDiff$pinName", 0); # time diff of last interval (old reading) - my $lastCDiff = ReadingsVal ($name, "countDiff$pinName", 0); # count diff of last interval (old reading) + my $lastInterval = ArduCounter_ReadingsVal ($name, "timeDiff", $pin); # time diff of last interval (old reading) + my $lastCDiff = ArduCounter_ReadingsVal ($name, "countDiff", $pin); # count diff of last interval (old reading) my $offlTime = sprintf ("%.2f", $hash->{deviceBooted} - $lastCountTNum); # estimated offline time (last report in readings until boot) - if ($lastInterval && ($offlTime > 0) && ($offlTime < 12*60*60)) { # offline > 0 and < 12h + if ($lastInterval && ($offlTime > 0) && ($offlTime < 12*60*60)) { # offline > 0 and < 12h my $lastRatio = $lastCDiff / $lastInterval; my $curRatio = $diff / $time; my $intRatio = 1000 * ($lastRatio + $curRatio) / 2; @@ -1379,123 +1877,170 @@ sub ArduCounter_HandleCounters($$$$$$$$) } else { Log3 $name, 4, "$pLog interpolation of missed pulses for pin $pinName ($lName) not possible - no valid historic data."; } + } else { + if ($pulseGap < 0) { # pulseGap < 0 abd not booted should not happen + Log3 $name, 3, "$pLog seems to have missed $seqGap reports in $timeGap seconds. " . + "Last reported sequence was $lastSeq, now $seq. " . + "Device count before was $lastCount, now $count with rDiff $rDiff " . + "but pulseGap is $pulseGap. this is probably wrong and should not happen. Setting pulseGap to 0." if (!$deviceBooted); + $pulseGap = 0; + } } Log3 $name, 3, "$pLog missed $seqGap reports in $timeGap seconds. Last reported sequence was $lastSeq, " . "now $seq. Device count before was $lastCount, now $count with rDiff $rDiff. " . "Adding $pulseGap to long count and intpolated count readings" if ($pulseGap > 0); - Log3 $name, 5, "$pLog adding rDiff $rDiff to long count $longCount and interpolated count $intpCount"; + Log3 $name, 5, "$pLog adding rDiff $rDiff to long count $longCount and interpolated count $intpCount" if ($rDiff); Log3 $name, 5, "$pLog adding interpolated $intrCount to interpolated count $intpCount" if ($intrCount); $intpCount += ($rDiff + $pulseGap + $intrCount); $longCount += ($rDiff + $pulseGap); - if ($ppk) { - $cCounter += ($rDiff + $pulseGap + $intrCount) / $ppk; # add to calculated counter - $iSum += $intrCount / $ppk; # sum of interpolation kWh + if ($ppu) { + $cCounter += ($rDiff + $pulseGap + $intrCount) / $ppu; # add to calculated counter + $iSum += $intrCount / $ppu; # sum of interpolation kWh } - readingsBulkUpdate($hash, $rcname, $count); # device internal counter - readingsBulkUpdate($hash, $rlname, $longCount); # Fhem long counterr - readingsBulkUpdate($hash, $riname, $intpCount); # Fhem interpolated counter - if ($ppk) { - readingsBulkUpdate($hash, $rccname, $cCounter); # Fhem calculated / interpolated counter - readingsBulkUpdate($hash, $rccname . "_i", $iSum); # Fhem interpolation sum - } - readingsBulkUpdate($hash, "seq".$pinName, $seq); # Sequence number + ArduCounter_BulkUpdate($hash, 'pin', $pin, $count); + ArduCounter_BulkUpdate($hash, 'long', $pin, $longCount); + ArduCounter_BulkUpdate($hash, 'interpolated', $pin, $intpCount); + ArduCounter_BulkUpdate($hash, 'calcCounter', $pin, $cCounter) if ($ppu); + ArduCounter_BulkUpdate($hash, 'calcCounter_i', $pin, $iSum) if ($ppu); + ArduCounter_BulkUpdate($hash, 'seq', $pin, $seq); } + + +######################################################################### +sub ArduCounter_HandleRunTime($$$$$) +{ + my ($hash, $pinName, $pin, $lastPower, $power) = @_; + my $name = $hash->{NAME}; + my $now = int(gettimeofday()); # just work with seconds here + #Log3 $name, 5, "$name: HandleRunTime: power is $power"; + if ($power > 0) { + my $soTime = ReadingsVal($name, ".switchOnTime$pinName", 0); # start time when power was >0 for the first time since it is >0 + if (!$soTime || !$lastPower) { + $soTime = $now; + readingsBulkUpdate($hash, ".switchOnTime$pinName", $now); # save when consumption started + readingsDelete($hash, "runTime$pinName"); + Log3 $name, 5, "$name: HandleRunTime: start from zero consumption - reset runtime and update .switchOnTime"; + } + if ($soTime) { + my $siTime = ReadingsVal($name, ".lastCheckIgnoreTime$pinName", $now); + my $iTime = ReadingsVal($name, "runTimeIgnore$pinName", 0); # time to ignore accumulated + + my $ignore = 0; + my $ignoreSpec = ArduCounter_AttrVal($hash, "", "runTimeIgnore$pinName", "runTimeIgnore$pin"); + my @devices = devspec2array($ignoreSpec); + #Log3 $name, 5, "$name: HandleRunTime: devices list is @devices"; + foreach my $d (@devices) { + my $state = (ReadingsVal($d, "state", "")); + #Log3 $name, 5, "$name: HandleRunTime: check $d with state $state"; + if ($state =~ /1|on|open|BI/) { + $ignore = 1; + Log3 $name, 5, "$name: HandleRunTime: ignoreDevice $d is $state"; + last; + } + } + my $iAddTime = 0; + if ($ignore) { # ignore device is on + $iAddTime = $now - $siTime; # add to ignore time + Log3 $name, 5, "$name: HandleRunTime: addiere $iAddTime auf ignoreTime $iTime"; + $iTime += $iAddTime; + readingsBulkUpdate($hash, "runTimeIgnore$pinName", $iTime); # remember time to ignore + readingsBulkUpdate($hash, ".lastCheckIgnoreTime$pinName", $now); + #Log3 $name, 5, "$name: HandleRunTime: setze .lastCheckIgnoreTime auf now"; + } else { + Log3 $name, 5, "$name: HandleRunTime: no ignoreDevice is on, lösche .lastCheckIgnoreTime"; + readingsDelete($hash, ".lastCheckIgnoreTime$pinName"); # no ignore device is on -> remove start marker + } + + my $rTime = int($now - $soTime); # time to add to runtime + my $newRunTime = $rTime - $iTime; + Log3 $name, 5, "$name: HandleRunTime: runTime is now: $rTime - $iTime = $newRunTime"; + readingsBulkUpdate($hash, "runTime$pinName", $newRunTime); + } + } else { + readingsDelete($hash, "runTime$pinName"); + readingsDelete($hash, "runTimeIgnore$pinName"); + readingsDelete($hash, ".switchOnTime$pinName"); + readingsDelete($hash, ".lastCheckIgnoreTime$pinName"); + } +} + + ######################################################################### sub ArduCounter_ParseReport($$) { my ($hash, $line) = @_; my $name = $hash->{NAME}; my $now = gettimeofday(); - if ($line =~ '^R([\d]+) C([\d]+) D([\d]+) ?[\/R]([\d]+) T([\d]+) N([\d]+),([\d]+) X([\d]+)( S[\d]+)?( A[\d]+)?') + if ($line =~ /^R(\d+) *C(\d+) *D(\d+) *[\/R](\d+) *T(\d+) *(N\d+,\d+)? *X(\d+)(?: *S(\d+))?(?: *A(\d+))?/) { # new count is beeing reported - my $pin = $1; - my $count = $2; # internal counter at device - my $diff = $3; # delta during interval - my $rDiff = $4; # real delta including the first pulse after a restart - my $time = $5; # interval in ms - my $deTime = $6; - my $deTiW = $7; - my $reject = $8; - my $seq = ($9 ? substr($9, 2) : ""); - my $avgLen = ($10 ? substr($10, 2) : ""); + my ($pin, $count, $diff, $rDiff, $time, $dTime, $reject, $seq, $avgLen) = + ($1, $2, $3, $4, $5, $6, $7, $8, $9); my $pinName = ArduCounter_PinName($hash, $pin); + my $power; - # now get pin specific reading names and options - first try with pinName, then pin Number, then generic fallback for all pins - my $factor = AduCounter_AttrVal($hash, 1000, "readingFactor$pinName", "readingFactor$pin", "factor"); - my $ppk = AduCounter_AttrVal($hash, 0, "readingPulsesPerKWh$pinName", "readingPulsesPerKWh$pin", "pulsesPerKWh"); - my $rpname = AduCounter_AttrVal($hash, "power$pinName", "readingNamePower$pinName", "readingNamePower$pin"); - my $lName = ArduCounter_LogPinDesc($hash, $pin); - my $pLog = "$name: pin $pinName ($lName)"; # start of log lines - - my $sTime = $now - $time/1000; # start of observation interval (~first pulse) in secs (floating point) - my $fSTime = FmtDateTime($sTime); # formatted - my $fSdTim = FmtTime($sTime); # only time formatted for logging - my $fEdTim = FmtTime($now); # end of Interval - only time formatted for logging + # first try with pinName, then pin Number, then generic fallback for all pins + my $factor = ArduCounter_AttrVal($hash, 1000, "readingFactor$pinName", "readingFactor$pin", "factor"); + my $ppu = ArduCounter_AttrVal($hash, 0, "readingPulsesPerKWh$pinName", "readingPulsesPerKWh$pin", "pulsesPerKWh"); + $ppu = ArduCounter_AttrVal($hash, $ppu, "readingPulsesPerUnit$pin", "readingPulsesPerUnit$pinName", "pulsesPerUnit"); + my $fut = ArduCounter_AttrVal($hash, 3600, "readingFlowUnitTime$pin", "readingFlowUnitTime$pinName", "flowUnitTime"); + my $doRTime = ArduCounter_AttrVal($hash, 0, "runTime$pinName", "runTime$pin"); + my $doSTime = ArduCounter_AttrVal($hash, 0, "readingStartTime$pinName", "readingStartTime$pin"); + my $lName = ArduCounter_LogPinDesc($hash, $pin); + my $pLog = "$name: pin $pinName ($lName)"; # start of log lines + my $sTime = $now - $time/1000; # start of interval (~first pulse) in secs (float) + my $fSdTim = FmtTime($sTime); # only time formatted for logging + my $fEdTim = FmtTime($now); # end of Interval - only time formatted for logging - ArduCounter_HandleDeviceTime($hash, $deTime, $deTiW, $now); + ArduCounter_ParseTime($hash, $dTime, $now) if (defined($dTime)); # parse device time (old firmware, now line starting with N) if (!$time || !$factor) { Log3 $name, 3, "$pLog skip line because time or factor is 0: $line"; return; } - my $power; - if ($ppk) { # new calculation with pulsee or rounds per unit (kWh) - $power = sprintf ("%.3f", ($time ? ($diff/$time) * (3600000 / $ppk) : 0)); - } else { # old calculation with a factor that is hard to understand - $power = sprintf ("%.3f", ($time ? $diff/$time/1000*3600*$factor : 0)); + if ($ppu) { + $power = ($diff/$time) * (1000 * $fut / $ppu); # new calculation with pulses or rounds per unit (kWh) + } else { + $power = ($diff/$time) / 1000 * $fut * $factor; # old calculation with a factor that is hard to understand } + my $powerFmt = sprintf ("%.3f", $power); Log3 $name, 4, "$pLog Cnt $count " . "(diff $diff/$rDiff) in " . sprintf("%.3f", $time/1000) . "s" . - " from $fSdTim until $fEdTim, seq $seq" . - ((defined($reject) && $reject ne "") ? ", Rej $reject" : "") . - (defined($avgLen) ? ", Avg ${avgLen}ms" : "") . - ", result $power"; + " from $fSdTim until $fEdTim, seq $seq" . ((defined($reject) && $reject ne "") ? ", Rej $reject" : "") . + (defined($avgLen) ? ", Avg ${avgLen}ms" : "") . (defined($ppu) ? ", PPU ${ppu}" : "") . + (defined($fut) ? ", FUT ${fut}s" : "") . ", result $powerFmt"; - if (AttrVal($name, "readingStartTime$pinName", AttrVal($name, "readingStartTime$pin", 0))) { - readingsBeginUpdate($hash); # special block: use time of interval start as reading time - Log3 $name, 5, "$pLog readingStartTime$pinName specified: setting timestamp to $fSdTim"; - my $chIdx = 0; - $hash->{".updateTime"} = $sTime; - $hash->{".updateTimestamp"} = $fSTime; - readingsBulkUpdate($hash, $rpname, $power) if ($time); - $hash->{CHANGETIME}[$chIdx++] = $fSTime; # Intervall start - readingsEndUpdate($hash, 1); # end of special block - readingsBeginUpdate($hash); # start regular update block - } else { - # normal way to set readings - readingsBeginUpdate($hash); # start regular update block - readingsBulkUpdate($hash, $rpname, $power) if ($time); - } - - - if (defined($reject) && $reject ne "") { - my $rejCount = ReadingsVal($name, "reject$pinName", 0); # alter reject count Wert - readingsBulkUpdate($hash, "reject$pinName", $reject + $rejCount); - } - readingsBulkUpdate($hash, "timeDiff$pinName", $time); # these readings are used internally for calculations - readingsBulkUpdate($hash, "countDiff$pinName", $diff); # these readings are used internally for calculations + my $lastPower = ArduCounter_ReadingsVal($name, 'power', $pin); # alter Power Wert + readingsBeginUpdate($hash); + ArduCounter_BulkUpdate($hash, 'power', $pin, $powerFmt, ($doSTime ? $sTime : undef)); - if (AttrVal($name, "verboseReadings$pinName", AttrVal($name, "verboseReadings$pin", 0))) { - readingsBulkUpdate($hash, "lastMsg$pinName", $line); + #Log3 $name, 5, "$pLog last power $lastPower, power $power"; + ArduCounter_HandleRunTime($hash, $pinName, $pin, $lastPower, $powerFmt) if ($doRTime); + + if (defined($reject) && $reject ne "") { + my $rejCount = ReadingsVal($name, "reject$pinName", 0); # alter reject count Wert + ArduCounter_BulkUpdate($hash, 'reject', $pin, $reject + $rejCount); } + ArduCounter_BulkUpdate($hash, 'timeDiff', $pin, $time); # used internally for interpolation + ArduCounter_BulkUpdate($hash, 'countDiff', $pin, $diff); # used internally for interpolation + ArduCounter_BulkUpdate($hash, 'lastMsg', $pin, $line); - ArduCounter_HandleCounters($hash, $pin, $seq, $count, $time, $diff, $rDiff, $now); + ArduCounter_HandleCounters($hash, $pin, $seq, $count, $time, $diff, $rDiff, $now, $ppu); readingsEndUpdate($hash, 1); - if (!$hash->{Initialized}) { # device has sent count but not Started / hello after reconnect + if (!$hash->{Initialized}) { # device sent count but no hello after reconnect Log3 $name, 3, "$name: device is still counting"; - if (!$hash->{WaitForHello}) { # if hello not already sent, send it now + if (!$hash->{WaitForHello}) { # if hello not already sent, send it now ArduCounter_AskForHello("direct:$name"); } - RemoveInternalTimer ("sendHello:$name"); # don't send hello again + RemoveInternalTimer ("sendHello:$name"); # don't send hello again } } } @@ -1505,27 +2050,28 @@ sub ArduCounter_HandleHistory($$$$) { my ($hash, $now, $pinName, $hist) = @_; my $name = $hash->{NAME}; - my @hList = split(/[, ]/, $hist); + my @hList = split(/, /, $hist); Log3 $name, 5, "$name: HandleHistory " . ($hash->{CL} ? "client $hash->{CL}{NAME}" : "no CL"); foreach my $he (@hList) { if ($he) { - if ($he =~ /([\d]+)s([\d-]+)\/([\d]+)@([01])(.)/) { - my ($seq, $time, $len, $level, $act) = ($1, $2, $3, $4, $5); + if ($he =~ /(\d+)[s\,]([\d\-]+)[\/\:](\d+)\@([01])(?:\/(\d+))?(.)/) { + my ($seq, $time, $len, $level, $alvl, $act) = ($1, $2, $3, $4, $5, $6); my $fTime = FmtDateTime($now + ($time/1000)); my $action =""; - if ($act eq "C") {$action = "count"} + if ($act eq "C") {$action = "pulse counted"} elsif ($act eq "G") {$action = "gap"} - elsif ($act eq "R") {$action = "reject"} - elsif ($act eq "X") {$action = "ignore spike"} - elsif ($act eq "P") {$action = "ignore drop"} - my $histLine = sprintf ("%6s", $seq) . ' ' . $fTime . " $pinName " . - sprintf ("%7s", sprintf("%.3f", $len/1000)) . " seconds at $level -> $action"; + elsif ($act eq "R") {$action = "short pulse reject"} + elsif ($act eq "X") {$action = "gap continued after ignored spike"} + elsif ($act eq "P") {$action = "pulse continued after ignored drop"} + my $histLine = "Seq " . sprintf ("%6s", $seq) . ' ' . $fTime . " Pin $pinName " . + sprintf ("%7s", sprintf("%.3f", $len/1000)) . " seconds at $level" . + (defined($alvl) ? " (analog $alvl)" : "") . " -> $action"; Log3 $name, 5, "$name: HandleHistory $histLine ($he)"; $hash->{LastHistSeq} = $seq -1 if (!defined($hash->{LastHistSeq})); $hash->{HistIdx} = 0 if (!defined($hash->{HistIdx})); - if ($seq > $hash->{LastHistSeq} || $seq < ($hash->{LastHistSeq} - 10000)) { + if ($seq > $hash->{LastHistSeq} || $seq < ($hash->{LastHistSeq} - 10000)) { # probably wrap $hash->{History}[$hash->{HistIdx}] = $histLine; $hash->{HistoryPin}[$hash->{HistIdx}] = $pinName; $hash->{LastHistSeq} = $seq; @@ -1536,9 +2082,7 @@ sub ArduCounter_HandleHistory($$$$) Log3 $name, 5, "$name: HandleHistory - no match for $he"; } } - } - } @@ -1553,98 +2097,101 @@ sub ArduCounter_Parse($) my $now = gettimeofday(); foreach my $line (@lines) { - #Log3 $name, 5, "$name: Parse line: $line"; - if ($line =~ /^R([\d]+)/) { - ArduCounter_ParseReport($hash, $line); - - } elsif ($line =~ /^H([\d]+) (.+)/) { # pin pulse history as separate line - my $pin = $1; - my $hist = $2; - my $pinName = ArduCounter_PinName($hash, $pin); - - ArduCounter_HandleHistory($hash, $now, $pinName, $hist); - - if (AttrVal($name, "verboseReadings$pinName", AttrVal($name, "verboseReadings$pin", 0))) { - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "pinHistory$pinName", $hist); - readingsEndUpdate($hash, 1); - } - - } elsif ($line =~ /^M Next report in ([\d]+)/) { # end of report tells when next - $retStr .= ($retStr ? "\n" : "") . $line; - Log3 $name, 4, "$name: device: $line"; - - } elsif ($line =~ /^I(.*)/) { # interval config report after show/hello - $hash->{runningCfg}{I} = $1; # save for later compare - $hash->{runningCfg}{I} =~ s/\s+$//; # remove spaces at end - $retStr .= ($retStr ? "\n" : "") . $line; - Log3 $name, 4, "$name: device sent interval config $hash->{runningCfg}{I}"; - - } elsif ($line =~ /^T(.*)/) { # analog threshold config report after show/hello - $hash->{runningCfg}{T} = $1; # save for later compare - $hash->{runningCfg}{T} =~ s/\s+$//; # remove spaces at end - $retStr .= ($retStr ? "\n" : "") . $line; - Log3 $name, 4, "$name: device sent analog threshold config $hash->{runningCfg}{T}"; - - } elsif ($line =~ /^V(.*)/) { # devVerbose - $hash->{runningCfg}{V} = $1; # save for later compare - $hash->{runningCfg}{V} =~ s/\s+$//; # remove spaces at end - $retStr .= ($retStr ? "\n" : "") . $line; - Log3 $name, 4, "$name: device sent devVerbose $hash->{runningCfg}{V}"; - - } elsif ($line =~ /^P([\d]+) (falling|rising|-) ?(pullup)? ?min ([\d]+)/) { # pin configuration at device - my $p = ($3 ? $3 : "nop"); - $hash->{runningCfg}{$1} = "$2 $p $4"; # save for later compare - $retStr .= ($retStr ? "\n" : "") . $line; - Log3 $name, 4, "$name: device sent config for pin $1: $2 $p min $4"; - - } elsif ($line =~ /^alive( ?RSSI ([\-\d]+))?/) { # alive response - Log3 $name, 5, "$name: device sent alive response: $line"; + $line =~ s/[\x0A\x0D]//g; + #Log3 $name, 5, "$name: Parse line: #" . $line . "#"; + + if ($line =~ /^ArduCounter V([\d\.]+).*(Started|Hello)/) { # setup / hello message + ArduCounter_ParseHello($hash, $line, $now); + + } elsif ($line =~ /^(?:A|a|alive|Alive) *(?:(?:R|RSSI) *([\-\d]+))? *$/) { # alive response + my $rssi = $1; + Log3 $name, 5, "$name: device sent alive response: $line" if (AttrVal($name, "logFilter", "N") =~ "N"); RemoveInternalTimer ("alive:$name"); $hash->{WaitForAlive} = 0; delete $hash->{KeepAliveRetries}; - if ($2) { - readingsBeginUpdate($hash); - readingsBulkUpdate($hash, "RSSI", $2); - readingsEndUpdate($hash, 1); - } + readingsSingleUpdate($hash, "RSSI", $rssi, 1) if ($rssi); - } elsif ($line =~ /^ArduCounter V([\d\.]+).*(Started|Hello)/) { # setup message - ArduCounter_ParseHello($hash, $line, $now); + } elsif ($line =~ /^R([\d]+)(.*)/) { # report counters + ArduCounter_ParseReport($hash, $line); + $retStr .= ($retStr ? "\n" : "") . "report for pin $1: $2"; + + } elsif ($line =~ /^C([0-9\,]+)/) { # available pins + ArduCounter_ParseAvailablePins($hash, $1); + $retStr .= ($retStr ? "\n" : "") . "available pins: $1"; - } elsif ($line =~ /^Status: ArduCounter V([\d\.]+)/) { # response to s(how) - $retStr .= ($retStr ? "\n" : "") . $line; + } elsif ($line =~ /^H([\d]+) (.+)/) { # pin pulse history as separate line + my $pin = $1; + my $hist = $2; + my $pinName = ArduCounter_PinName($hash, $pin); + ArduCounter_HandleHistory($hash, $now, $pinName, $hist); + if (ArduCounter_AttrVal($hash, 0, "verboseReadings$pinName", "verboseReadings$pin") eq "1") { + readingsSingleUpdate($hash, "pinHistory$pinName", $hist, 1); + } + + } elsif ($line =~ /^I(.*)/) { # interval config report after show/hello + $retStr .= ($retStr ? "\n" : "") . "interval config: $1"; + $hash->{runningCfg}{I} = $1; # save for later compare + $hash->{runningCfg}{I} =~ s/\s+$//; # remove spaces at end + Log3 $name, 4, "$name: device sent interval config $hash->{runningCfg}{I}"; + + } elsif ($line =~ /^U(.*)/) { # unit display config + $retStr .= ($retStr ? "\n" : "") . "display unit config: $1"; + $hash->{runningCfg}{U} = $1; # save for later compare + $hash->{runningCfg}{U} =~ s/\s+$//; # remove spaces at end + Log3 $name, 4, "$name: device sent unit display config $hash->{runningCfg}{U}"; + + } elsif ($line =~ /^V(.*)/) { # devVerbose + $retStr .= ($retStr ? "\n" : "") . "verbose config: $1"; + $hash->{runningCfg}{V} = $1; # save for later compare + $hash->{runningCfg}{V} =~ s/\s+$//; # remove spaces at end + Log3 $name, 4, "$name: device sent devVerbose $hash->{runningCfg}{V}"; - } elsif ($line =~ /connection already busy/) { + } elsif ($line =~ /^P(\d+) *(f|falling|r|rising|-) *(p|pullup)? *(?:m|min)? *(\d+) *(?:(?:analog)? *(?:o|out|out-pin)? *(\d+) *(?:(?:t|thresholds) *(\d+) *[\/\, ] *(\d+))?)?(?:, R\d+.*)?/) { # pin configuration at device + my $p = ($3 ? $3 : "nop"); + $hash->{runningCfg}{$1} = $line; + $retStr .= ($retStr ? "\n" : "") . "pin $1 config: $2 $p min length $4 " . ($5 ? "analog out $5 thresholds $6/$7" : ""); + Log3 $name, 4, "$name: device sent config for pin $1: $line"; + + } elsif ($line =~ /^N(.*)/) { # device time and boot time, track drift + ArduCounter_ParseTime($hash, $line, $now); + $retStr .= ($retStr ? "\n" : "") . "device time $1"; + Log3 $name, 4, "$name: device sent time info: $line"; + + } elsif ($line =~ /conn.* busy/) { my $now = gettimeofday(); my $delay = AttrVal($name, "nextOpenDelay", 60); Log3 $name, 4, "$name: _Parse: primary tcp connection seems busy - delay next open"; - ArduCounter_Disconnected($hash); # set to disconnected (state), remove timers - DevIo_CloseDev($hash); # close, remove from readyfnlist so _ready is not called again + ArduCounter_Disconnected($hash); # set to disconnected (state), remove timers + DevIo_CloseDev($hash); # close, remove from readyfnlist so _ready is not called again RemoveInternalTimer ("delayedopen:$name"); InternalTimer($now+$delay, "ArduCounter_DelayedOpen", "delayedopen:$name", 0); - } elsif ($line =~ /^D (.*)/) { # debug / info Message from device - $retStr .= ($retStr ? "\n" : "") . $line; - Log3 $name, 4, "$name: device: $1"; - - } elsif ($line =~ /^L *([\d]+) ?, ?([\d]+) ?, ?-> *([\d]+)/) { # analog level difference reported with details + # todo: the level reports should be recorded separately per pin + } elsif ($line =~ /^L\d+: *([\d]+) ?, ?([\d]+) ?, ?-> *([\d]+)/) { # analog level difference reported with details if ($hash->{analogLevels}{$3}) { $hash->{analogLevels}{$3}++; } else { $hash->{analogLevels}{$3} = 1; } - } elsif ($line =~ /^L *([\d]+)/) { # analog level difference reported + } elsif ($line =~ /^L\d+: *([\d]+)/) { # analog level difference reported if ($hash->{analogLevels}{$1}) { $hash->{analogLevels}{$1}++; } else { $hash->{analogLevels}{$1} = 1; } + } elsif ($line =~ /^Error:/) { # Error message from device + $retStr .= ($retStr ? "\n" : "") . $line; + Log3 $name, 3, "$name: device: $line"; + } elsif ($line =~ /^M (.*)/) { # other Message from device - $retStr .= ($retStr ? "\n" : "") . $line; + $retStr .= ($retStr ? "\n" : "") . $1; Log3 $name, 3, "$name: device: $1"; + } elsif ($line =~ /^D (.*)/) { # debug / info Message from device + $retStr .= ($retStr ? "\n" : "") . $1; + Log3 $name, 4, "$name: device: $1"; + } elsif ($line =~ /^[\s\n]*$/) { # blank line - ignore } else { @@ -1729,17 +2276,13 @@ sub ArduCounter_ReadAnswer($$) return ("No data", undef); } } - if($buf) { #Log3 $name, 5, "$name: ReadAnswer got: $buf"; $hash->{buffer} .= $buf; } - my $end = chop $buf; #Log3 $name, 5, "$name: Current buffer content: " . $hash->{buffer}; next if ($end ne "\n"); - - $msgBuf .= "\n" if ($msgBuf); $msgBuf .= ArduCounter_Parse($hash); @@ -1756,31 +2299,44 @@ sub ArduCounter_ReadAnswer($$) 1; - =pod =item device -=item summary Module for counters based on arduino / ESP8266 board -=item summary_DE Modul für Strom / Wasserzähler mit Arduino- oder ESP8266 +=item summary Module for energy / water meters on arduino / ESP8266 / ESP32 +=item summary_DE Modul für Strom / Wasserzähler mit Arduino, ESP8266 oder ESP32 =begin html

ArduCounter


@@ -1811,54 +2367,74 @@ sub ArduCounter_ReadAnswer($$) Configuration of ArduCounter digital counters


Configuration of ArduCounter analog counters


@@ -1918,24 +2498,53 @@ sub ArduCounter_ReadAnswer($$)
  • raw
  • send the value to the board so you can directly talk to the sketch using its commands.
    This is not needed for normal operation but might be useful sometimes for debugging -
  • flash
  • - flashes the ArduCounter firmware ArduCounter.hex from the fhem subdirectory FHEM/firmware - onto the device. This command needs avrdude to be installed. The attribute flashCommand specidies how avrdude is called. If it is not modifed then the module sets it to avrdude -p atmega328P -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]
    - This setting should work for a standard installation and the placeholders are automatically replaced when - the command is used. So normally there is no need to modify this attribute.
    - Depending on your specific Arduino board however, you might need to insert -b 57600 in the flash Command. (e.g. for an Arduino Nano) - ESP boards so far have to be fashed from the Arduino IDE. In a future version flashing over the air sould be supported. +
  • flash [<file>]
  • + flashes the ArduCounter firmware from the subdirectory FHEM/firmware onto the device.
    + Normally you can just specify set myDevice flash. The parameter <file> is optional and allows specifying an alternative firmware file. + The attribute flashCommand can be used to override which command is executed. + If the attribute flashCommand is not specified then the module selects an appropriate command depending on the board type + (set with the attribute board) and depending on the connection (serial or Wifi).
    + For an arduino NANO for example the module would execute avrdude (which has to be installed of course) + and flash the connected arduino with the updated hex file
    + (by default it looks for ArduCounter.hex in the FHEM/firmware subdirectory).
    + For an Arduino UNO for example the default is avrdude -p atmega328P -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]
    + For an Arduino Nano based counter -b 57600 is added.
    + For an ESP32 connected via Wifi, the module would call espota.py which will upload the firmware over the air.
    + If the attribute flashCommand is not specified for an ESP32 based board connected via serial line, then the module uses the command + + esptool.py --chip esp32 --port [PORT] --baud 460800 --before default_reset --after hard_reset write_flash -z + --flash_mode dio --flash_freq 40m --flash_size detect + 0x1000 FHEM/firmware/ArduCounter_ESP32_bootloader_dio_40m.bin + 0x8000 FHEM/firmware/ArduCounter_ESP32_partitions.bin + 0xe000 FHEM/firmware/ArduCounter_ESP32_boot_app0.bin + 0x10000 FHEM/firmware/ArduCounter_ESP32_firmware.bin >[LOGFILE] 2>&1 + for example which flashes the whole ESP32 with all the partitions.
    + For over the air flashing it would use + espota.py -i[IP] -p [NETPORT] -f [BINFILE] 2>[LOGFILE].
    + Of course esptool.py or espota.py as well as python would need to be installed on the system. + +
  • resetWifi
  • + reset Wifi settings of the counting device so the Wifi Manager will come up after the next reset to select a wireless network and enter the Wifi passphrase.
  • reset
  • - reopens the arduino device and sends a command to it which causes a reinitialize and reset of the counters. Then the module resends the attribute configuration / definition of the pins to the device. + sends a command to the device which causes a hardware reset or reinitialize and reset of the internal counters of the board.
    + The module then reopens the counting device and resends the attribute configuration / definition of the pins.
  • saveConfig
  • - stores the current interval, analog threshold and pin configuration to be stored in the EEPROM of the counter device so it can be retrieved after a reset. + stores the current interval, analog threshold and pin configuration in the EEPROM of the counter device so it will automatically be retrieved after a reset.
  • enable
  • sets the attribute disable to 0
  • disable
  • sets the attribute disable to 1
  • reconnect
  • closes the tcp connection to an ESP based counter board that is conected via TCP/IP and reopen the connection - +
  • clearLevels
  • + clears the statistics for analog levels. This is only relevant if you use the board to read via a reflective light barrier + and you want to set the thresholds according to the statistics. +
  • clearCounters <pin>
  • + resets all the counter readings for the specified pin to 0 +
  • counter <pin>, <value>
  • + set the calcCounter reading for the specified pin to the given value +
  • clearHistory
  • + deletes all the cached pin history entries
    @@ -1946,10 +2555,10 @@ sub ArduCounter_ReadAnswer($$) This is not needed for normal operation but might be useful sometimes for debugging
  • levels
  • show the count for the measured levels if an analog pin is used to measure e.g. the red mark of a ferraris counter disc. This is useful for setting the thresholds for analog measurements. -
  • history
  • +
  • history <pin>
  • shows details regarding all the level changes that the counter device (Arduino or ESP) has detected and how they were used (counted or rejected)
    If get history is issued with a pin name (e.g. get history D5) then only the history entries concerning D5 will be shown.
    - This information is sent from the device to Fhem when it reports the current count but only if devVerbose is equal or greater than 5.
    + This information is sent from the device to Fhem if the attribute enableHistory is set to 1.
    The maximum number of lines that the Arducounter module stores in a ring buffer is defined by the attribute maxHist and defaults to 1000.
    @@ -1959,18 +2568,31 @@ sub ArduCounter_ReadAnswer($$)
  • do_not_notify
  • readingFnAttributes

  • -
  • pin[AD]?[0-9]+
  • - Define a pin of the Arduino or ESP board as input. This attribute expects either - rising, falling or change, followed by an optional pullup and an optional number as value.
    - If a number is specified, the arduino will track rising and falling edges of each impulse and measure the length of a pulse in milliseconds. The number specified here is the minimal length of a pulse and a pause before a pulse. If one is too small, the pulse is not counted but added to a separate reject counter.
    +
  • pin[AD]?[0-9]+<rising|falling> [<pullup>] [min] <min length> [[analog] out <out pin> [threshold] <min, max>]
  • + Define a GPIO pin of the Arduino or ESP board as input. This attribute expects for digital inputs either + rising or falling, followed by an optional pullup and the optional keyword min + and an optional number as minimal length of pulses and gaps between pulses.
    + The counter device will track rising and falling edges of each impulse and measure the length of a pulse in milliseconds.
    + The minimal length specified here is the minimal duration of a pulse and a pause before a pulse. If one is too small, + the pulse is not counted but added to a separate reject counter.
    Example:
    - attr MyCounter pinD4 falling pullup 30 + attr MyCounter pinD4 falling pullup 25 + + For analog inputs with connected reflective light barries, you have to add analog out + and the GPIO pin number of the pin where the light source (LED or laser) is connected, the keyword threshold + followed by the lower and upper threshold separated by a komma.
    + Example:
    + + attr MyCounter pinA0 rising pullup min 3 analog out 27 threshold 120,220 -
  • interval normal max min mincout
  • +
  • interval <normal> <max> [<min> <min count> [<analog interval> <analog samples>]]
  • Defines the parameters that affect the way counting and reporting works. - This Attribute expects at least two and a maximum of four numbers as value. The first is the normal interval, the second the maximal interval, the third is a minimal interval and the fourth is a minimal pulse count. + This Attribute expects at least two and a maximum of six numbers as value. + The first is the normal interval, the second the maximal interval, the third is a minimal interval and the fourth is a minimal pulse count. + The last two numbers are only needed for counting with reflective light barriers. They specify the delay between the measurements + and the number of samples for each measurement.

    In the usual operation mode (when the normal interval is smaller than the maximum interval), the Arduino board just counts and remembers the time between the first impulse and the last impulse for each pin.
    @@ -1978,117 +2600,159 @@ sub ArduCounter_ReadAnswer($$) This means that even though the normal interval might be 10 seconds, the reported time difference can be something different because it observed impulses as starting and ending point.
    The Power (e.g. for energy meters) is then calculated based of the counted impulses and the time between the first and the last impulse.
    - For the next interval, the starting time will be the time of the last impulse in the previous reporting period and the time difference will be taken up to the last impulse before the reporting interval has elapsed. + For the next interval, the starting time will be the time of the last impulse in the previous reporting period + and the time difference will be taken up to the last impulse before the reporting interval has elapsed.

    - The second, third and fourth numbers (maximum, minimal interval and minimal count) exist for the special case when the pulse frequency is very low and the reporting time is comparatively short.
    - For example if the normal interval (first number) is 60 seconds and the device counts only one impulse in 90 seconds, the the calculated power reading will jump up and down and will give ugly numbers.
    + The second, third and fourth numbers (maximum, minimal interval and minimal count) exist for the special case + when the pulse frequency is very low and the reporting time is comparatively short.
    + For example if the normal interval (first number) is 60 seconds and the device counts only one impulse in 90 seconds, + the the calculated power reading will jump up and down and will give ugly numbers.
    By adjusting the other numbers of this attribute this can be avoided.
    - In case in the normal interval the observed impulses are encountered in a time difference that is smaller than the third number (minimal interval) or if the number of impulses counted is smaller than the fourth number (minimal count) then the reporting is delayed until the maximum interval has elapsed or the above conditions have changed after another normal interval.
    + In case in the normal interval the observed impulses are encountered in a time difference that is smaller than the third number (minimal interval) + or if the number of impulses counted is smaller than the fourth number (minimal count) then the reporting is delayed until the maximum interval has elapsed + or the above conditions have changed after another normal interval.
    This way the counter will report a higher number of pulses counted and a larger time difference back to fhem.
    Example:
    attr myCounter interval 60 600 5 2
    - If this is seems too complicated and you prefer a simple and constant reporting interval, then you can set the normal interval and the mximum interval to the same number. This changes the operation mode of the counter to just count during this normal and maximum interval and report the count. In this case the reported time difference is always the reporting interval and not the measured time between the real impulses. + If this is seems too complicated and you prefer a simple and constant reporting interval, then you can set the normal interval and the mximum interval to the same number. + This changes the operation mode of the counter to just count during this normal and maximum interval and report the count. + In this case the reported time difference is always the reporting interval and not the measured time between the real impulses. +

    + For analog sampling the last two numbers define the delay in milliseconds between analog measurements and the number of samples that will be taken as one mesurement. -
  • factor
  • - Define a multiplicator for calculating the power from the impulse count and the time between the first and the last impulse.
    - This attribute is outdated and unintuitive so you should avoid it.
    - Instead you should specify the attribute pulsesPerKWh or readingPulsesPerKWh[0-9]+ (where [0-9]+ stands for the pin number). - -
  • readingFactor[0-9]+
  • - Override the factor attribute for this individual pin.
    - Just like the attribute factor, this is a rather cumbersome way to specify the pulses per kWh.
    - Instaed it is advised to use the attribute pulsesPerKWh or readingPulsesPerKWh[0-9]+ (where [0-9]+ stands for the pin number). - -
  • pulsesPerKWh
  • - specify the number of pulses that the meter is giving out per unit that sould be displayed (e.g. per kWh energy consumed). For many S0 counters this is 1000, for old ferraris counters this is 75 (rounds per kWh).
    +
  • pulsesPerUnit <number>
  • + specify the number of pulses that the meter is giving out per unit that sould be displayed (e.g. per kWh energy consumed).
    + For many S0 counters this is 1000, for old ferraris counters this is 75 (rounds per kWh).
    + This attribute used to be called pulsesPerKWh and this name still works but the new name should be used preferably since the old one could be removed in future versions.
    Example: - attr myCounter pulsesPerKWh 75 + attr myCounter pulsesPerUnit 75 -
  • readingPulsesPerKWh[0-9]+
  • - is the same as pulsesPerKWh but specified per pin individually in case you have multiple counters with different settings at the same time +
  • readingPulsesPerUnit[AD]?[0-9]+ <number>
  • + is the same as pulsesPerUnit but specified per GPIO pin individually in case you have multiple counters with different settings at the same time
    + This attribute used to be called readingPulsesPerKWh[AD]?[0-9]+ and this name still works but the new name should be used preferably + since the old one could be removed in future versions.

    Example:
    - attr myCounter readingPulsesPerKWhA7 75
    - attr myCounter readingPulsesPerKWhD4 1000 + attr myCounter readingPulsesPerUnitA7 75
    + attr myCounter readingPulsesPerUnitD4 1000
    -
  • readingNameCount[AD]?[0-9]+
  • +
  • readingFlowUnitTime[AD]?[0-9]+ <time>
  • + specified the time period in seconds which is used as the basis for calculating the current flow or power for the given pin.
    + If the counter e.g. counts liters and you want to see the flow in liters per minute, then you have to set this attribute to 60.
    + If you count kWh and you want to see the current power in kW, then specify 3600 (one hour).
    + Since this attribute is just used for multiplying the consumption per second, you can also use it to get watts + instead of kW by using 3600000 instead of 3600. + +
  • flowUnitTime <time>
  • + like readingFlowUnitTimeXX but applies to all pins that have no explicit readingFlowUnitTimeXX attribute. + +
  • readingNameCount[AD]?[0-9]+ <new name>
  • Change the name of the counter reading pinX to something more meaningful.
    Example: attr myCounter readingNameCountD4 CounterHaus_internal -
  • readingNameLongCount[AD]?[0-9]+
  • +
  • readingNameLongCount[AD]?[0-9]+ <new name>
  • Change the name of the long counter reading longX to something more meaningful.
    Example: attr myCounter readingNameLongCountD4 CounterHaus_long -
  • readingNameInterpolatedCount[AD]?[0-9]+
  • +
  • readingNameInterpolatedCount[AD]?[0-9]+ <new name>
  • Change the name of the interpolated long counter reading InterpolatedlongX to something more meaningful.
    Example: attr myCounter readingNameInterpolatedCountD4 CounterHaus_interpolated -
  • readingNameCalcCount[AD]?[0-9]+
  • +
  • readingNameCalcCount[AD]?[0-9]+ <new name>
  • Change the name of the real unit counter reading CalcCounterX to something more meaningful.
    Example: attr myCounter readingNameCalcCountD4 CounterHaus_kWh -
  • readingNamePower[AD]?[0-9]+
  • +
  • readingNamePower[AD]?[0-9]+ <new name>
  • Change the name of the power reading powerX to something more meaningful.
    Example: - attr myCounter readingNamePowerD4 PowerHaus + attr myCounter readingNamePowerD4 PowerHaus_kW -
  • readingStartTime[AD]?[0-9]+
  • +
  • readingStartTime[AD]?[0-9]+ [0|1]
  • Allow the reading time stamp to be set to the beginning of measuring intervals. + This is a hack where the timestamp of readings is artificially set to a past time and may have side effects + so avoid it unless you fully understand how Fhem works with readings and their time. -
  • verboseReadings[AD]?[0-9]+
  • - create readings timeDiff, countDiff, lastMsg and pinHistory for each pin
    +
  • verboseReadings[AD]?[0-9]+ [0|1]
  • + create the additional readings lastMsg and pinHistory for each pin
    + if verboseReafings is set to 1 for the specified pin.
    + If set to -1 then the internal counter, the long counter and interpolated long counter readings will be hidden.
    Example: attr myCounter verboseReadingsD4 1 -
  • devVerbose
  • - set the verbose level in the counting board. This defaults to 0.
    - If the value is >0, then the firmware will echo all commands sent to it by the Fhem module.
    - If the value is >=5, then the firmware will report the pin history (assuming that the firmware has been compiled with this feature enabled)
    - If the value is >=10, then the firmware will report every level change of a pin
    - If the value is >=20, then the firmware will report every analog measurement (assuming that the firmware has been compiled with analog measurements for old ferraris counters or similar). +
  • enableHistory [0|1]
  • + tells the counting device to record the individual time of each change at each GPIO pin and send it to Fhem. + This information is cached on the Fhem side and can be viewed with the command get history + The optput of get history will look like this: + + Seq 12627 2020-03-22 20:39:54 Pin D5 0.080 seconds at 0 -> pulse counted + Seq 12628 2020-03-22 20:39:55 Pin D5 1.697 seconds at 1 -> gap + Seq 12629 2020-03-22 20:39:56 Pin D5 0.080 seconds at 0 -> pulse counted + Seq 12630 2020-03-22 20:39:56 Pin D5 1.694 seconds at 1 -> gap + Seq 12631 2020-03-22 20:39:58 Pin D5 0.081 seconds at 0 -> pulse counted + Seq 12632 2020-03-22 20:39:58 Pin D5 1.693 seconds at 1 -> gap + Seq 12633 2020-03-22 20:40:00 Pin D5 0.081 seconds at 0 -> pulse counted + Seq 12634 2020-03-22 20:40:00 Pin D5 1.696 seconds at 1 -> gap + Seq 12635 2020-03-22 20:40:02 Pin D5 0.081 seconds at 0 -> pulse counted + Seq 12636 2020-03-22 20:40:02 Pin D5 1.699 seconds at 1 -> gap + Seq 12637 2020-03-22 20:40:03 Pin D5 0.079 seconds at 0 -> pulse counted + Seq 12638 2020-03-22 20:40:03 Pin D5 1.700 seconds at 1 -> gap + Seq 12639 2020-03-22 20:40:05 Pin D5 0.080 seconds at 0 -> pulse counted + Seq 12642 2020-03-22 20:40:05 Pin D5 1.699 seconds at 1 -> gap + Seq 12643 2020-03-22 20:40:07 Pin D5 0.080 seconds at 0 -> pulse counted + Seq 12644 2020-03-22 20:40:07 Pin D5 1.698 seconds at 1 -> gap + + +
  • enableSerialEcho [0|1]
  • + tells the counting device to show diagnostic data over the serial line when connected via TCP -
  • maxHist
  • +
  • enablePinDebug [0|1]
  • + tells the counting device to show every level change of the defined input pins over the serial line or via TCP +
  • enableAnalogDebug [0|1]
  • + tells the counting device to show every analog measurement of the defined analog input pins over the serial line or via TCP +
  • enableDevTime [0|1]
  • + tells the counting device to show its internal millis timer so a drift between the devices time and fhem time can be calculated and logged + +
  • maxHist <max entries>
  • specifies how many pin history lines hould be buffered for "get history".
    This attribute defaults to 1000.
  • analogThresholds
  • - this Attribute is necessary when you use an arduino nano with connected reflection light barrier (photo transistor and led) to detect the red mark of an old ferraris energy counter. In this case the firmware uses an upper and lower threshold which can be set here.
    - Example: - - attr myCounter analogThresholds 90 110 -
    - In order to find out the right threshold values you can set devVerbose to 20, wait for several turns of the ferraris disc and then use get levels to see the typical measurements for the red mark and the blank disc. + this Attribute is outdated. Please specify the analog thresholds for reflective light barrier input with the attribute "pin..." -
  • flashCommand
  • - sets the command to call avrdude and flash the onnected arduino with an updated hex file (by default it looks for ArduCounter.hex in the FHEM/firmware subdirectory.
    - This attribute contains avrdude -p atmega328P -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE] by default.
    - For an Arduino Nano based counter you should add -b 57600 e.g. between the -P and -D options.
    +
  • flashCommand <new shell command>
  • + overrides the default command to flash the firmware via Wifi (OTA) or serial line. It is recommended to not define this attribute.
    Example: - + attr myCounter flashCommand avrdude -p atmega328P -c arduino -b 57600 -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE] - + + [PORT] is automatically replaced with the serial port for this device as it is specified in the define command.
    + [HEXFILE] or [BINFILE] are synonyms and are both automatically replaced with the firmware file appropriate for the device. + For ESP32 boards [HEXFILE] would be replaced by ArduCounter-8266.bin for example.
    + [LOGFILE] is automatically replaced ArduCounterFlash.log in the fhem log subdirectory.
    + [NETPORT] is automatically replaced by the tcp port number used for OTA flashing. + For ESP32 this usually is 3232 and for 8266 Bords it is 8266.
    -
  • keepAliveDelay
  • +
  • keepAliveDelay <delay>
  • defines an interval in which the module sends keepalive messages to a counter device that is conected via tcp.
    This attribute is ignored if the device is connected via serial port.
    If the device doesn't reply within a defined timeout then the module closes and tries to reopen the connection.
    @@ -2099,50 +2763,80 @@ sub ArduCounter_ReadAnswer($$) attr myCounter keepAliveDelay 30 -
  • keepAliveTimeout
  • +
  • keepAliveTimeout <seconds>
  • defines the timeout when wainting for a keealive reply (see keepAliveDelay) The timeout defaults to 2 seconds.
    Example: attr myCounter keepAliveTimeout 3 -
  • keepAliveRetries
  • +
  • keepAliveRetries <max number of retries>
  • defines how often sending a keepalive is retried before the connection is closed and reopened.
    It defaults to 2.
    Example: attr myCounter keepAliveRetries 3 -
  • nextOpenDelay
  • - defines the time that the module waits before retrying to open a disconnected tcp connection.
    +
  • nextOpenDelay <delay>
  • + defines the time in seconds that the module waits before retrying to open a disconnected tcp connection.
    This defaults to 60 seconds.
    Example: attr myCounter nextOpenDelay 20 -
  • openTimeout
  • - defines the timeout after which tcp open gives up trying to establish a connection to the counter device. +
  • openTimeout <timeout>
  • + defines the timeout in seconds after which tcp open gives up trying to establish a connection to the counter device. This timeout defaults to 3 seconds.
    Example: attr myCounter openTimeout 5 -
  • silentReconnect
  • +
  • silentReconnect [0|1]
  • if set to 1, then it will set the loglevel for "disconnected" and "reappeared" messages to 4 instead of 3
    Example: attr myCounter silentReconnect 1 -
  • disable
  • - if set to 1 then the module closes the connection to a counter device.
    + +
  • deviceDisplay <pin> <unit> <flowUnit>
  • + controls the unit strings that a local display on the counting device will show.
    + Example: + + attr myCounter deviceDisplay 36,l,l/m + attr myCounter deviceDisplay 36,kWh,kW + + +
  • disable [0|1]
  • + if set to 1 then the module is disabled and closes the connection to a counter device.
    + +
  • factor
  • + Define a multiplicator for calculating the power from the impulse count and the time between the first and the last impulse.
    + This attribute is outdated and unintuitive so you should avoid it.
    + Instead you should specify the attribute pulsesPerUnit or readingPulsesPerUnit[0-9]+ (where [0-9]+ stands for the pin number). + +
  • readingFactor[AD]?[0-9]+
  • + Override the factor attribute for this individual pin.
    + Just like the attribute factor, this is a rather cumbersome way to specify the pulses per kWh.
    + Instead it is advised to use the attribute pulsesPerUnit or readingPulsesPerUnit[0-9]+ (where [0-9]+ stands for the pin number). + +
  • devVerbose
  • + this attribute is outdated and has been replaced with the attributes + enableHistory, enableSerialEcho, enablePinDebug, enableAnalogDebug, enableDevTime
    Readings / Events

    diff --git a/fhem/FHEM/firmware/ArduCounter-ESP32-boot_app0.bin b/fhem/FHEM/firmware/ArduCounter-ESP32-boot_app0.bin new file mode 100755 index 000000000..13562cabb Binary files /dev/null and b/fhem/FHEM/firmware/ArduCounter-ESP32-boot_app0.bin differ diff --git a/fhem/FHEM/firmware/ArduCounter-ESP32-bootloader_dio_40m.bin b/fhem/FHEM/firmware/ArduCounter-ESP32-bootloader_dio_40m.bin new file mode 100755 index 000000000..ac057886d Binary files /dev/null and b/fhem/FHEM/firmware/ArduCounter-ESP32-bootloader_dio_40m.bin differ diff --git a/fhem/FHEM/firmware/ArduCounter-ESP32-partitions.bin b/fhem/FHEM/firmware/ArduCounter-ESP32-partitions.bin new file mode 100755 index 000000000..0e52b4c4e Binary files /dev/null and b/fhem/FHEM/firmware/ArduCounter-ESP32-partitions.bin differ diff --git a/fhem/FHEM/firmware/ArduCounter-ESP32.bin b/fhem/FHEM/firmware/ArduCounter-ESP32.bin new file mode 100755 index 000000000..f98a37921 Binary files /dev/null and b/fhem/FHEM/firmware/ArduCounter-ESP32.bin differ diff --git a/fhem/FHEM/firmware/ArduCounter-ESP32T.bin b/fhem/FHEM/firmware/ArduCounter-ESP32T.bin new file mode 100755 index 000000000..b8fc57481 Binary files /dev/null and b/fhem/FHEM/firmware/ArduCounter-ESP32T.bin differ diff --git a/fhem/FHEM/firmware/ArduCounter-ESP8266.bin b/fhem/FHEM/firmware/ArduCounter-ESP8266.bin new file mode 100755 index 000000000..7c530b38e Binary files /dev/null and b/fhem/FHEM/firmware/ArduCounter-ESP8266.bin differ diff --git a/fhem/FHEM/firmware/ArduCounter-NANO.hex b/fhem/FHEM/firmware/ArduCounter-NANO.hex new file mode 100755 index 000000000..433bcb21d --- /dev/null +++ b/fhem/FHEM/firmware/ArduCounter-NANO.hex @@ -0,0 +1,1010 @@ +:100000000C94CE010C94F6010C94F6010C94420F62 +:100010000C941D0F0C94F80E0C94F6010C94F60140 +:100020000C94F6010C94F6010C94F6010C94F60174 +:100030000C94F6010C94F6010C94F6010C94F60164 +:100040000C945A140C94F6010C9430160C940A1665 +:100050000C94F6010C94F6010C94F6010C94F60144 +:100060000C94F6010C94F6018A032B097204BF0468 +:10007000A8052B092B09EE06F1062B0980079F071F +:100080002B092B092B092B09D307DA07DD077B087E +:100090002B09E3082C0043002048656C6C6F00209E +:1000A000004420616E616C6F6720746872657368CC +:1000B0006F6C64732073657420746F200044204556 +:1000C0004550524F4D20436F6E6669673A20004499 +:1000D000206E6F20636F6E66696720696E204545EC +:1000E00050524F4D00206D696C6C697365636F6E83 +:1000F00064730044204E657874207265706F72746A +:1001000020696E20002C20004420537461747573A4 +:100110003A20004420544350206E6F7420636F6E69 +:100120006E65637465640041004420636F6E6669A8 +:100130006720736176656400442072656D6F766533 +:100140006420004D20636C6561726564200044206A +:10015000646566696E65642000746F6F206D616E02 +:100160007920616E616C6F672070696E7300207317 +:10017000697A6520002C004420676F7420004D20B0 +:10018000726573746F72696E6720636F6E666967FC +:100190002066726F6D20454550524F4D3A20004DFC +:1001A000206E6F20636F6E66696720696E2045451B +:1001B00050524F4D002000636F6D6D616E642000E2 +:1001C0004572726F723A2000496C6C6567616C20F1 +:1001D00070696E2073706563696669636174696FC5 +:1001E0006E20004A756E20203620323032302031A9 +:1001F000323A34303A32390020636F6D70696C6581 +:100200006420004E414E4F00206F6E20004172640A +:1002100075436F756E7465722056342E3236002C1D +:100220002072656A65637420002C20636F756E749C +:1002300020002073657120002C2068697374496464 +:10024000782000206368616E67656420746F200009 +:100250004D2070696E200029206973206F7574200D +:100260006F6620626F756E647300202876616C750E +:10027000652000706172616D65746572206E756DC8 +:1002800062657220006D697373696E6720706172B8 +:10029000616D65746572206E756D62657220002CEB +:1002A0000042002C004E002C002C002C002C002CB6 +:1002B0000049002C002C002C002C0056002F00744C +:1002C000006F757400206D0070002D0066007200D4 +:1002D000500041005300580054002F0044004300D8 +:1002E000520044004572726F723A2077726F6E67E7 +:1002F00020616E616C6F6720726561642073746148 +:100300007465002C20002C20312C20004D20002C66 +:1003100020002C20302C20004D2000FFFF02030481 +:1003200005060708090A0B0C0D0E0F101112131405 +:10033000152F0040003A002C002C2000200048001F +:1003400000000000240027002A0000000000250013 +:1003500028002B00000000002300260029000404D0 +:10036000040404040404020202020202030303035D +:100370000303010204081020408001020408102039 +:100380000102040810200000000800020100000320 +:10039000040700000000000000009E1B11241FBE87 +:1003A000CFEFD8E0DEBFCDBF11E0A0E0B1E0E4EADE +:1003B000FEE302C005900D92A835B107D9F725E0FC +:1003C000A8E5B1E001C01D92A83FB207E1F711E036 +:1003D000CEECD1E004C02197FE010E94411CCD3C2F +:1003E000D107C9F70E9464160C94501F0C940000AA +:1003F000AF92BF92CF92DF92EF92FF920F931F9333 +:10040000CF93DF9300D000D000D0CDB7DEB75C0132 +:10041000042F7F936F938FE291E09F938F93CE0190 +:1004200001967C019F938F930E94661CF7010190B7 +:100430000020E9F731971E2F1E190F900F900F9093 +:100440000F900F900F90163060F467018C2D8E196D +:10045000811738F4F60161916F01C5010E94C90945 +:10046000F5CF002321F06CE2C5010E94C909269650 +:100470000FB6F894DEBF0FBECDBFDF91CF911F91B5 +:100480000F91FF90EF90DF90CF90BF90AF900895C5 +:10049000CF92DF92EF92FF920F931F93CF930E9420 +:1004A0004E146B017C018091CF019091D0010E948C +:1004B000F4150E94E00F68E970E08091CF019091FF +:1004C000D0010E94F815C701B6010E94010D66E92E +:1004D00070E08091CF019091D0010E94BB150BE19B +:1004E00013E081E0C0E0F80194919F3F91F0811109 +:1004F00008C064E970E08091CF019091D0010E9422 +:10050000BB156C2F8091CF019091D0010E945F1399 +:1005100080E0CF5F0F5F1F4FC63129F78091CF0179 +:100520009091D0010E94F4150E94650C0E940F0C5E +:10053000C0E01DE280912105C81778F41C9FC0011E +:1005400011248B5F9C4F0E9406118091CF019091E6 +:10055000D0010E94F415CF5FEDCFCF911F910F9185 +:10056000FF90EF90DF90CF9008952F923F924F929F +:100570005F926F927F929F92AF92BF92CF92DF92E3 +:10058000EF92FF920F931F93CF93DF9300D0CDB7DD +:10059000DEB7E82F8C3221F5E0919F01EF3008F0B3 +:1005A000E4C681E08E0F80939F0104E0E09FF0019C +:1005B0001124E55AFE4F80919B0190919C01A091DE +:1005C0009D01B0919E0180839183A283B383109299 +:1005D0009B0110929C0110929D0110929E01C5C634 +:1005E00080ED8E0F8A3048F520919B0130919C015F +:1005F00040919D0150919E01AAE0B0E00E94471CED +:10060000AB01BC0140535109610971098E2FEE0FF6 +:10061000990BAA0BBB0B840F951FA61FB71F8093C6 +:100620009B0190939C01A0939D01B0939E018091AA +:100630009F018F5F80935A0198C68FE98E0F8A3190 +:1006400008F093C6E0935901A0919F01AF3098F450 +:1006500014E0A19FD0011124A55ABE4F40919B01E7 +:1006600050919C0160919D0170919E014D935D930D +:100670006D937C9313971E2F809158018230D0F197 +:1006800067E771E08CE295E00E94BB155BE5C52E43 +:1006900051E0D52EE12CF12C80919F0190E08E1538 +:1006A0009F05BCF0E114F10431F065E771E08CE2E4 +:1006B00095E00E94BB15F6014191519161917191B4 +:1006C0006F018CE295E00E942A13FFEFEF1AFF0AF8 +:1006D000E3CF612F8CE295E00E94B4156EE671E0E5 +:1006E0008CE295E00E94BB1560915A018CE295E086 +:1006F0000E946613812F110F990BAA0BBB0BFC01F3 +:10070000E156F109E631F10508F0A5C5EC5CFF4FB3 +:100710000C94411C40905B0150905C0160905D0125 +:1007200070905E01142D0FEF4016510461047104A6 +:1007300011F008F093C5842D0E949211882309F4CA +:100740008DC59091C90129EC35E080E0981761F0E2 +:100750002D5E3F4FF90170974081141711F08F5FA4 +:10076000F5CF8F3F09F07AC5E42DF0E0E55EFC4F50 +:10077000E490842D0E949E11F82E1FEF81130EC06D +:10078000F09021050E944E14AB01BC012DE2F29EB7 +:10079000C00111248B5F9C4F0E94B21121E043E005 +:1007A00050E062E070E081E00E943310882309F499 +:1007B00052C5AF2CB12C3DE2F39EF0011124EB5F4A +:1007C000FC4F81E040915F015091600160916101B7 +:1007D00070916201433051056105710509F080E0B7 +:1007E00082838DE28A9D80018B9D100D11240B5F09 +:1007F0001C4FF801408220E041E050E060E070E0F2 +:1008000082E00E943310882329F080916301F8016F +:10081000838302C0F801138220E048EE53E061E0D8 +:1008200070E083E00E94331081110CC082E090E000 +:10083000A0E0B0E08093670190936801A093690104 +:10084000B0936A012DE22A9DC0012B9D900D1124C9 +:100850008C010B5F1C4F680180916701F801818357 +:1008600020E046E150E061E070E084E00E94331057 +:10087000882379F090906B01892D0E949211882332 +:1008800009F4EEC4892D0E949E118F3F09F0E8C43F +:10089000EBC4F8018381882311F062E001C060E0BD +:1008A0008E2D0E94D6138F2D0E949E0D8DE28A9D63 +:1008B000F0018B9DF00D1124EB5FFC4F4082809185 +:1008C0002105F81203C0F394F09221056EE471E063 +:1008D0008091CF019091D0010E94BB15C6010E946A +:1008E000061128C3C0905B01D0905C01E0905D01CF +:1008F000F0905E018C2D0E949E11FFEFCF16D10467 +:10090000E104F10411F008F01AC58F3F09F417C58E +:10091000082F10E00E944E142DE2209FF001219F2D +:10092000F00D1124EB5FFC4F67A370A781A792A77E +:1009300011A212A213A214A216A215A2F8948189E0 +:100940009289A389B48985879687A787B08B1582FA +:10095000168217821086118E1186128613861486CF +:10096000158A168A178A108E789463E471E0809154 +:10097000CF019091D0010E94BB156C2DE2C0409038 +:100980005B0150905C0160905D0170905E01842D70 +:100990000E949E11182F0FEF40165104610471043C +:1009A00011F008F058C48F3F09F455C4E82EF12C1B +:1009B0002DE2829FF0011124EB5FFC4F84818111B5 +:1009C00004C0812F0E945B0D3FC08091C901A9EC3A +:1009D000B5E090E08917C1F12D913C9111975396A4 +:1009E0002E173F0711F09F5FF5CF9F3F69F1282F2A +:1009F00030E003E1692F70E0AB014F5F5F4F4217BA +:100A00005307ACF4069FD001079FB00D1124A753E4 +:100A1000BA4F049FF001059FF00D1124E753FA4FE0 +:100A2000402F01900D924A95E1F79F5FE3CF8150EF +:100A30008093C90133E1839FF0011124E753FA4FFA +:100A400011821082128213820DE20E9DC0010F9D51 +:100A5000900D1124FC01E75FFC4F6F01E12EF12C9A +:100A60005701FFEFAF1ABF0A80912105282F30E010 +:100A7000A216B3060CF04EC00E9DF0010F9DF00DB6 +:100A80001124EB5FFC4F8481811104C081E0810F50 +:100A90000E945B0D0E9DC0010F9D900D11248B5F78 +:100AA0009C4F0A9DF0010B9DF00D1124EB5FFC4F54 +:100AB000DC01202F01900D922A95E1F7FC01848141 +:100AC000811103C0812F0E949E0DF6018081882331 +:100AD000F9F09091C901E9ECF5E080E00A9D900100 +:100AE0000B9D300D11242B5F3C4F0E9DA0010F9DDF +:100AF000500D11244B5F5C4F981751F0608171814C +:100B00006217730711F4518340838F5F7396F4CF9C +:100B10001F5FA4CF815080932105FDE28F9FC0010C +:100B2000112440E050E0BA018B5F9C4F0E94B2114B +:100B300068E371E08091CF019091D0010E94BB15D4 +:100B4000642D8091CF019091D0010E94661383C3E0 +:100B50001A82198263E4CE0101960E94C90966E6F1 +:100B6000CE0101960E94C90962E3CE0101960E945E +:100B7000C90960910D0170910E0180910F01909152 +:100B8000100128EEC22E23E0D22EE12CF12CA70179 +:100B900096010E941F1CCA01B90141E0CE010196D5 +:100BA0000E94F8016091090170910A0180910B0186 +:100BB00090910C01A70196010E941F1CCA01B90166 +:100BC00041E0CE0101960E94F8016091020170910E +:100BD00003018091040190910501A70196010E94F3 +:100BE0001F1CCA01B90141E0CE0101960E94F80123 +:100BF000609100017091010141E0CE0101960E94D7 +:100C0000F801609107017091080141E0CE01019661 +:100C10000E94F8016091060170E040E0CE0101966B +:100C20000E94F80169E6CE0101960E94C90960910F +:100C3000040370E041E0CE0101960E94F80160914A +:100C4000580170E041E0CE0101960E94F8016091E8 +:100C5000220570E041E0CE0101960E94F80160910A +:100C6000C80170E041E0CE0101960E94F801609158 +:100C7000C50170E040E0CE0101960E94F80166E7F0 +:100C8000CE0101960E94C909F12C3DE2E32E43E119 +:100C9000942E80912105F81608F098C0CF2CD12C05 +:100CA000EC9C8001ED9C100D11240B5F1C4FC801C2 +:100CB0000E94EE10F8019481992309F44CC08F3FF3 +:100CC00009F449C0A82EB12C608170E041E0CE014A +:100CD00001960E94F801F8018281811103C062E04F +:100CE00070E002C063E070E041E0CE0101960E9436 +:100CF000F801EC9C8001ED9C100D11240B5F1C4F42 +:100D0000F801638170E041E0CE0101960E94F80194 +:100D1000F801618170E041E0CE0101960E94F80186 +:100D20009A9C80019B9C100D112407531A4FF801C7 +:100D3000638170E041E0CE0101960E94F801F80164 +:100D40006481758141E0CE0101960E94F801F801AD +:100D5000668177812FC0EC9C8001ED9C100D1124E1 +:100D60000B5F1C4FF801608170E041E0CE010196FD +:100D70000E94F801F8018281811103C062E070E0F5 +:100D800002C063E070E041E0CE0101960E94F801EC +:100D9000EC9C8001ED9C100D11240B5F1C4FF801A1 +:100DA000638170E041E0CE0101960E94F801F801F4 +:100DB000618170E040E0CE0101960E94F80161E699 +:100DC000CE0101960E94C909F39463CF60E0CE0181 +:100DD00001960E94C90969E271E088C10E94480237 +:100DE0003AC221E040E15EE061E070E080E00E9414 +:100DF0003310882309F42FC221E040E15EE061E076 +:100E000070E081E00E943310882309F424C221E0BD +:100E100040E15EE060E070E082E00E9433108823F1 +:100E200009F419C221E044E650E060E070E083E09C +:100E30000E943310882309F40EC220915B01309187 +:100E40005C0140915D0150915E01A8EEB3E00E940B +:100E5000471C60930D0170930E0180930F019093D6 +:100E6000100120915F01309160014091610150912A +:100E700062010E94471C6093090170930A018093EC +:100E80000B0190930C01209163013091640140911A +:100E90006501509166010E94471C609302017093A6 +:100EA0000301809304019093050180916701909163 +:100EB0006801909301018093000120E040E157E236 +:100EC00060E070E084E00E943310882341F080915C +:100ED0006B0190916C01909308018093070120E0D1 +:100EE00044E650E061E070E085E00E943310882322 +:100EF00021F080916F01809306010E94650CABC1C7 +:100F000080915B0190915C01A0915D01B0915E01C7 +:100F10000197A105B10509F09EC180915A0188236E +:100F200009F499C167E271E08091CF019091D001FD +:100F30000E94BB158091CF019091D00141C1C0901A +:100F40005B01D0905C01E0905D01F0905E011C2D92 +:100F5000FFEFCF16D104E104F10411F008F0EFC166 +:100F6000CC2009F4ECC18C2D0E949211882309F445 +:100F7000E6C19091C90129EC35E080E0981761F055 +:100F80002D5E3F4FF90170974081141711F08F5F6C +:100F9000F5CF8F3F09F0D3C18C2D0E949E118F3F5A +:100FA00009F0CDC158C163E171E08CE295E00E9487 +:100FB000F81551C10E94E9114EC10E944E142B0137 +:100FC0003C018091CF019091D0010E94F41568E01E +:100FD00071E08091CF019091D0010E94BB150E94D9 +:100FE000E00F8091CF019091D0010E94F4150E94F2 +:100FF000650C0E940F0C10E00DE28091210518177E +:10100000D0F4019FC00111249C012B5F3C4F79015A +:10101000C9010E94061165E071E08091CF019091B5 +:10102000D0010E94BB15A301920161E0C7010E949B +:10103000EC091F5FE2CF80E090E00E946216009111 +:10104000CF011091D001833409F07BC181E090E0A1 +:101050000E946216863609F074C182E090E00E9418 +:101060006216823309F06DC16DEB70E0C8010E9419 +:10107000BB1583E090E08C010F5F1F4F0E9462164A +:10108000811165C18091CF019091D0010E94F4152A +:1010900063EF70E08091CF019091D0010E94BB1569 +:1010A000C0900003D0900103E0900203F09003038E +:1010B00080910D0190910E01A0910F01B09110014E +:1010C000C80ED91EEA1EFB1E0E944E14970186010F +:1010D000061B170B280B390BB901A8018091CF0112 +:1010E0009091D0010E942A1365EE70E08091CF01AB +:1010F0009091D0015CCF21E04FEF53E061E070E0D0 +:1011000080E00E943310882309F4A5C021E04FEF4E +:1011100053E061E070E081E00E943310882309F41D +:101120009AC08091C90120915B0130915C018823B4 +:10113000C1F03093CE052093CD05823020F030935E +:10114000E1052093E00520915F013091600130932B +:10115000D0052093CF05823020F03093E305209313 +:10116000E20561EA70E08091CF019091D0010E9488 +:10117000BB1540915B0150915C0160915D017091E4 +:101180005E018091CF019091D0010E942A136FE9F6 +:1011900070E08091CF019091D0010E94BB15009129 +:1011A000CF011091D00140915F0150916001609199 +:1011B0006101709162012AE0C8010E94B812C80161 +:1011C0000E94F41548C021E041E050E060E070E08A +:1011D00080E00E9433108823F1F121E04AE050E0E2 +:1011E00060E070E081E00E9433108823A1F121E0EB +:1011F00041E050E060E070E082E00E94331088231C +:1012000051F121E04AE050E060E070E083E00E94AC +:101210003310882301F121E041E050E060E070E00C +:1012200084E00E9433108823B1F080915B018093A9 +:10123000040380915F018093580180916301809342 +:101240002205809167018093C80180916B01809392 +:10125000C5010E940F0C0E94D71087C0842D01C0C9 +:10126000892D0E94C310F7CFC8010E94EE108F3F56 +:10127000D1F48091C901823088F491E0980F909365 +:10128000C90103E1809FF0011124E753FA4F158251 +:10129000148217821682D182C08205C00E94131068 +:1012A00069E571E023CF9DE29A9DF0019B9DF00DD1 +:1012B0001124EB5FFC4F91E09483282E312C23E125 +:1012C000829F8001112407531A4FF80142829382B2 +:1012D00060E08E2D0E94D613E92DF0E0E55EFC4F14 +:1012E000849161E00E94D61321E04FEF53E061E06A +:1012F00070E085E00E943310882339F080916F01FF +:1013000090917001F8019583848321E04FEF53E0C1 +:1013100061E070E086E00E943310882309F4C6CAB9 +:1013200083E1829DF001839DF00D1124E753FA4F74 +:10133000809173019091740197838683B7CA8C2D35 +:1013400090CF6FEC70E0C8010E94F815A1CE682F15 +:101350008091CF019091D0010E94B415C8018115F0 +:1013600012E0910709F48ECE86CE0F900F90DF9198 +:10137000CF911F910F91FF90EF90DF90CF90BF9092 +:10138000AF909F907F906F905F904F903F902F9085 +:101390000895FF920F931F93CF93DF93EC01F62EE6 +:1013A00008811981C8010E94621690E02F2D0F2C30 +:1013B000000C330B8217930721F06F2DC8010E9498 +:1013C000E91B88819981019699838883DF91CF9168 +:1013D0001F910F91FF9008952F923F924F925F922D +:1013E0006F927F928F929F92AF92BF92CF92DF9235 +:1013F000EF92FF921F93CF93DF93CDB7DEB7A29703 +:101400000FB6F894DEBF0FBECDBF9A8F898F162F0F +:10141000F894A98DBA8D1D968D909D90AD90BC903D +:10142000509789829A82AB82BC8251968D909D9012 +:10143000AD90BC9054978D829E82AF82B8861596EF +:101440008D909D90AD90BC9018978F8E98A2A9A278 +:10145000BAA219968D909D90AD90BC901C978986EC +:101460009A86AB86BC8659966C91599755968D9005 +:101470009D90AD90BC9058978D8A9E8AAF8AB88E09 +:1014800078944D805E806F80788489809A80AB806C +:10149000BC80481859086A087B08E98DFA8D81A13B +:1014A00092A1A3A1B4A18F8C98A0A9A0BAA0881AD8 +:1014B000990AAA0ABB0A8D869E86AF86B88A7501EC +:1014C0006401C61AD108E108F10825A036A011115F +:1014D000D1C087A190A5A1A5B2A549015A01881A3A +:1014E000990AAA0ABB0A8B8E9C8EAD8EBE8E809105 +:1014F000090190910A01A0910B01B0910C01898B17 +:101500009A8BAB8BBC8B88159905AA05BB0508F097 +:101510004EC08091000190910101A0E0B0E0C8169A +:10152000D906EA06FB0660F1809102019091030161 +:10153000A0910401B091050184159505A605B70594 +:10154000F8F480900D0190900E01A0900F01B090E2 +:10155000100189899A89AB89BC8988159905AA05E2 +:10156000BB0571F0F8948D809E80AF80B884A98D02 +:10157000BA8D1D968D929D92AD92BC9250974EC0A1 +:10158000F894E98DFA8D258736874787508B218B14 +:10159000328B438B548B789429013A0189809A804D +:1015A000AB80BC80481859086A087B0838C0809115 +:1015B0000D0190910E01A0910F01B09110018B8C43 +:1015C0009C8CAD8CBE8C88159905AA05BB0508F0CE +:1015D0000EC18091000190910101A0E0B0E0C81619 +:1015E000D906EA06FB0608F402C180910201909137 +:1015F0000301A0910401B091050184159505A6058C +:10160000B70508F0F4C0F8948D819E81AF81B8854C +:10161000E98DFA8D85879687A787B08B7894F894A3 +:10162000A98DBA8D59961C925997FD01158A168A73 +:10163000178A108E78948F8C98A0A9A0BAA0A98D33 +:10164000BA8D91968D929D92AD92BC929497E985B8 +:10165000FA859696FC93EE93959797962D933D93E6 +:101660004D935C939A979B968C919B978F5F9B96DB +:101670008C9308C062EE72E08091CF019091D0010E +:101680000E94BB1560EE72E08091CF019091D00175 +:101690000E94BB15E98DFA8D60818091CF019091F8 +:1016A000D0010E945F136EED72E08091CF019091A6 +:1016B000D0010E94BB154F8D58A169A17AA18091DC +:1016C000CF019091D0010E942A136CED72E08091BD +:1016D000CF019091D0010E94BB15B701A601809166 +:1016E000CF019091D0010E942A136AED72E080919F +:1016F000CF019091D0010E94BB154D855E856F850D +:1017000078898091CF019091D0010E942A1368EDD1 +:1017100072E08091CF019091D0010E94BB15B3017E +:10172000A2018091CF019091D0010E942A1366ED11 +:1017300072E08091CF019091D0010E94BB15698524 +:101740007A85621973098091CF019091D0010E942E +:101750007813111112C064ED72E08091CF01909165 +:10176000D0010E94BB15A98DBA8D9B966C9180917A +:10177000CF019091D0010E945F13C114D104E10404 +:10178000F104C1F062ED72E08091CF019091D0013F +:101790000E94BB156D897E898F89988DA70196015E +:1017A0000E941F1CBA01A9018091CF019091D00124 +:1017B0000E942A138091CF019091D001A2960FB67A +:1017C000F894DEBF0FBECDBFDF91CF911F91FF9088 +:1017D000EF90DF90CF90BF90AF909F908F907F90D1 +:1017E0006F905F904F903F902F900C94F415A296BD +:1017F0000FB6F894DEBF0FBECDBFDF91CF911F9122 +:10180000FF90EF90DF90CF90BF90AF909F908F9020 +:101810007F906F905F904F903F902F9008956BEB6B +:1018200072E08091CF019091D0010E94BB15609130 +:1018300004038091CF019091D0010E945F1369EB66 +:1018400072E08091CF019091D0010E94BB15609110 +:1018500058018091CF019091D0010E945F1367EBF6 +:1018600072E08091CF019091D0010E94BB156091F0 +:1018700022058091CF019091D0010E945F1365EB0A +:1018800072E08091CF019091D0010E94BB156091D0 +:10189000C8018091CF019091D0010E945F1363EB4A +:1018A00072E08091CF019091D0010E94BB156091B0 +:1018B000C5018091CF019091D0010E945F1380916A +:1018C000CF019091D0010C94F415CF92DF92EF925A +:1018D000FF9261EB72E08091CF019091D0010E9464 +:1018E000BB1560910D0170910E0180910F019091D7 +:1018F000100128EEC22E23E0D22EE12CF12CA701FC +:1019000096010E941F1CBA01A9018091CF019091FC +:10191000D0010E942A136FEA72E08091CF0190916A +:10192000D0010E94BB156091090170910A0180915C +:101930000B0190910C01A70196010E941F1CBA0196 +:10194000A9018091CF019091D0010E942A136DEAE4 +:1019500072E08091CF019091D0010E94BB156091FF +:101960000201709103018091040190910501A7018A +:1019700096010E941F1CBA01A9018091CF0190918C +:10198000D0010E942A136BEA72E08091CF019091FE +:10199000D0010E94BB1560910001709101018091FE +:1019A000CF019091D0010E94781369EA72E0809192 +:1019B000CF019091D0010E94BB15609107017091F9 +:1019C00008018091CF019091D0010E94781367EABD +:1019D00072E08091CF019091D0010E94BB1560917F +:1019E00006018091CF019091D0010E945F138091F8 +:1019F000CF019091D001FF90EF90DF90CF900C94A9 +:101A0000F415CF92DF92EF92FF926B017C0165EAB1 +:101A100072E08091CF019091D0010E94BB15B70177 +:101A2000A6018091CF019091D0010E942A1363EA10 +:101A300072E08091CF019091D0010E94BB1560911E +:101A4000C6017091C7018091CF019091D0010E9491 +:101A5000781361EA72E08091CF019091D0010E94E9 +:101A6000BB154091BD015091BE016091BF017091C5 +:101A7000C0018091CF019091D0010E942A136FE99B +:101A800072E08091CF019091D0010E94BB156091CE +:101A9000BB017091BC018091CF019091D0010E9457 +:101AA00078138091CF019091D001FF90EF90DF905B +:101AB000CF900C94F4152DE2829FC0011124FC01FB +:101AC000EB5FFC4F208130E0F901E55EFC4FE491D3 +:101AD0008E2F90E0FC01EE58FC4F4491FC01E25A3D +:101AE000FC4F8491882331F1F901EE55FE4F9FEFB1 +:101AF00090838250E82FF0E0EE0FFF1FE95EFE4F6B +:101B0000A081B1819C91E42FE095E923EC939C9115 +:101B1000911112C04091680021E030E0B90102C08B +:101B2000660F771F8A95E2F7CB0180958423809317 +:101B3000680002C080E0089581E008959DE2899FD9 +:101B4000F0011124EB5FFC4F408150E0FA01E55EAB +:101B5000FC4FE4912E2F30E0F901EE58FC4F6491D8 +:101B6000F901E25AFC4F2491222359F1FA01EE5572 +:101B7000FE4F80832250822F90E0FC01EE0FFF1F6A +:101B8000E85AFC4FA591B4913C91FC01E854FE4FFA +:101B90003083880F991FDC01A95EBE4F0D90BC9168 +:101BA000A02DEC91E62BEC933091680081E090E061 +:101BB00001C0880F2A95EAF7832B8093680081E0A3 +:101BC000089580E008954F925F926F927F928F9276 +:101BD0009F92AF92BF92FF920F931F93CF93DF9389 +:101BE000EC01F62E8A010E944E14AB01BC018B8DD4 +:101BF0009C8DAD8DBE8D4A015B01881A990AAA0A97 +:101C0000BB0A898190E0A0E0B0E02A81881699069D +:101C1000AA06BB0680F48F8D82134EC089859A85F3 +:101C2000AB85BC850196A11DB11D89879A87AB87BD +:101C3000BC8782E542C08F8D82133AC098A1891774 +:101C400049F18D819E81AF81B8850196A11DB11D9D +:101C50008D839E83AF83B887498B5A8B6B8B7C8B2C +:101C60008A8D811109C04D875E876F87788B81E0EF +:101C70008A8F898D8F5F898F8D899E89AF89B88D75 +:101C8000880D991DAA1DBB1D8D8B9E8BAF8BB88FA8 +:101C900083E40FC08D899E89AF89B88D880D991D09 +:101CA000AA1DBB1D8D8B9E8BAF8BB88F80E501C0AD +:101CB00087E49F8D98A301C088E590910403992340 +:101CC00081F19091FF029F5F9093FF02943110F099 +:101CD0001092FF02E091FF022091A0013091A1013A +:101CE000D9011196B093A101A093A0019FE0E99FB3 +:101CF000F0011124ED52FE4F3183208398819283AD +:101D00004B8C5C8C6D8C7E8C468257826086718693 +:101D10009F8D93831583048382869386A486B586DC +:101D200086874B8F5C8F6D8F7E8FFF8EDF91CF917B +:101D30001F910F91FF90BF90AF909F908F907F90D9 +:101D40006F905F904F900895EF92FF920F931F93C3 +:101D5000CF93DF9390E0FC01EE0FFF1FE85AFC4F9A +:101D6000A591B4910C91DC01A854BE4F1C91FC01CB +:101D7000EE0FFF1FE95EFE4F0190F081E02D208104 +:101D8000102712230C93112361F1FC01EC5EFE4F2E +:101D9000C081D1E0FC01EF5EFE4FF0808DE2E82EC5 +:101DA000FC16F8F08D2F8123C9F0EC2FF0E0EE55F2 +:101DB000FE4F208130E02F3F310581F061E08D2F13 +:101DC000802309F460E0E29EC001E39E900D11249F +:101DD00040E050E08B5F9C4F0E94E30DDD0FCF5F32 +:101DE000DFCFDF91CF911F910F91FF90EF9008957A +:101DF0001F920F920FB60F9211242F933F934F9380 +:101E00005F936F937F938F939F93AF93BF93EF9362 +:101E1000FF9382E00E94A40EFF91EF91BF91AF91DA +:101E20009F918F917F916F915F914F913F912F91F2 +:101E30000F900FBE0F901F9018951F920F920FB624 +:101E40000F9211242F933F934F935F936F937F9340 +:101E50008F939F93AF93BF93EF93FF9381E00E9483 +:101E6000A40EFF91EF91BF91AF919F918F917F91C0 +:101E70006F915F914F913F912F910F900FBE0F90F7 +:101E80001F9018951F920F920FB60F9211242F9347 +:101E90003F934F935F936F937F938F939F93AF93F2 +:101EA000BF93EF93FF9380E00E94A40EFF91EF9108 +:101EB000BF91AF919F918F917F916F915F914F9162 +:101EC0003F912F910F900FBE0F901F9018951F9369 +:101ED000CF93DF93EC01162F6CA760E572E0809141 +:101EE000CF019091D0010E94BB1568818091CF01F4 +:101EF0009091D0010E945F1363E472E08091CF0162 +:101F00009091D0010E94BB15612F8091CF019091DB +:101F1000D0010E945F1368E372E08091CF0190913D +:101F2000D0010E94BB156091FF028091CF0190917A +:101F3000D0010E945F1362E372E08091CF01909123 +:101F4000D0010E94BB15E091FF028FE0E89FF001F5 +:101F50001124ED52FE4F608171818091CF019091EB +:101F6000D0010E94781369E272E08091CF019091D4 +:101F7000D0010E94BB154D815E816F817885809173 +:101F8000CF019091D0010E942A136FE172E08091FD +:101F9000CF019091D0010E94BB1549855A856B8570 +:101FA0007C858091CF019091D0010E942A1380916D +:101FB000CF019091D001DF91CF911F910C94F41536 +:101FC000CF93DF93CDE0D2E0FE0164918091CF0109 +:101FD0009091D0010E94B4152196F2E0CE31DF0736 +:101FE00099F768E072E08091CF019091D0010E9452 +:101FF000BB1563E072E08091CF019091D0010E9407 +:10200000BB1568EF71E08091CF019091D0010E94E3 +:10201000BB1563EE71E08091CF019091D001DF910B +:10202000CF910C94BB1560EC71E08091CF01909141 +:10203000D0010E94BB1567EB71E08091CF019091B8 +:10204000D0010E94BB15609159018091CF01909100 +:10205000D0010E94B41565EB71E08091CF019091A1 +:10206000D0010C94BB158F929F92AF92BF92EF92CA +:10207000FF920F931F93CF93DF93C82F80915A0144 +:10208000C81700F1222309F470C00E94131065E8FC +:1020900072E08091CF019091D0010E94BB15009118 +:1020A000CF011091D0014C2F50E04F5F5F4F052EB4 +:1020B000000C660B770BC8010E942D13C8010E940B +:1020C000F41553C00C2F10E07801EE0CFF1CEE0C41 +:1020D000FF1CF701E55AFE4F80809180A280B380FB +:1020E000CB01A0E0B0E088169906AA06BB0650F026 +:1020F00041155105E1F160E070E0481559056A05A8 +:102100007B05A8F50E94131063E772E08091CF0170 +:102110009091D0010E94BB15E8012196AE01DD0F20 +:10212000660B770B8091CF019091D0010E942D1307 +:102130006AE672E08091CF019091D0010E94BB15B8 +:10214000F701E55AFE4F4081518162817381809190 +:10215000CF019091D0010E942A1367E572E080912F +:10216000CF019091D0010E94F81580E001C081E07C +:10217000DF91CF911F910F91FF90EF90BF90AF90A3 +:102180009F908F900895CF93C82F0E94131068ECF2 +:1021900071E08091CF019091D0010E94BB156C2F0E +:1021A0008091CF019091D001CF910C946613109241 +:1021B0009F0110925A0110929B0110929C01109263 +:1021C0009D0110929E01EBE5F1E0119211921192A6 +:1021D000119281E0EB39F807C1F70895BC014091F5 +:1021E000C90129EC35E090E0941761F02D5E3F4F76 +:1021F000F90173970190F081E02DE617F70721F0C0 +:102200009F5FF2CF8FEF0895892F08950F931F934B +:10221000CF93DF93EC0160ED72E08091CF0190915C +:10222000D0010E94BB1568818091CF019091D001AF +:102230000E945F132A818091CF019091D0012223C7 +:1022400029F0213031F46EEC72E005C06CEC72E0E4 +:1022500002C06AEC72E00E94BB158B81882341F0BA +:1022600068EC72E08091CF019091D0010E94BB1583 +:1022700065EC72E08091CF019091D0010E94BB1576 +:1022800069818091CF019091D0010E945F138C8170 +:10229000882309F442C0CE010E94EE108F3FE9F17D +:1022A000082F10E061EC72E08091CF019091D00195 +:1022B0000E94BB1583E1809FE001819FD00D112416 +:1022C000C753DA4F6B818091CF019091D0010E946A +:1022D0005F136FEB72E08091CF019091D0010E946B +:1022E000BB156C817D818091CF019091D0010E94BE +:1022F00078136DEB72E08091CF019091D0010E9434 +:10230000BB156E817F818091CF019091D001DF91CB +:10231000CF911F910F910C947813DF91CF911F9162 +:102320000F910895863140F4E82FF0E0E55EFC4F10 +:10233000E49181E0EF3F09F480E0089540912105A8 +:1023400025E033E090E0941749F0235D3F4FF90119 +:10235000BD975081581721F09F5FF5CF8FEF0895FB +:10236000892F0895CF93DF93EC018FEF88831A8E26 +:1023700019821D821E821F821886198E19A21AA226 +:102380001BA21CA219861A861B861C861EA21DA2D1 +:102390004D875E876F87788B498B5A8B6B8B7C8BD5 +:1023A0004B8F5C8F6D8F7E8F4FA358A769A77AA73D +:1023B0001BA68C81811106C0EAE1F4E084910E94A1 +:1023C0007E1301C080E08F8F88A38CA7DF91CF910F +:1023D0000895CF92DF92EF92FF920F931F93CF93C6 +:1023E000DF930E944E146B017C016093BD017093DA +:1023F000BE018093BF019093C0018091C60190916E +:10240000C7019093BC018093BB01C0920003D0929E +:102410000103E0920203F0920303C092CB01D09239 +:10242000CC01E092CD01F092CE011092D201109237 +:10243000D101C092C101D092C201E092C301F092D9 +:10244000C4011092040310925801109222051092B8 +:10245000C8011092C501109221051092C901109275 +:10246000CA011092FF0281E090E09093A101809355 +:10247000A00180E090E04FE03FEF20E2489FF001B4 +:10248000499FF00D1124ED52FE4F118210823283CC +:10249000138216821782108611861286138614867E +:1024A0001586268701968431910541F7C5E0D3E072 +:1024B00001E215E0B701A601CE010E94B211AD966E +:1024C0000C171D07B9F7E2EAF1E08FEF819321E0E5 +:1024D000E83BF207D9F780E090E00E946216C091D5 +:1024E000CF01D091D001833461F481E090E00E946B +:1024F0006216863631F482E090E00E9462168233E2 +:1025000079F06FE971E0CE010E94F815DF91CF916B +:102510001F910F91FF90EF90DF90CF900C94D71008 +:102520006EE771E0CE010E94F81583E090E07C0137 +:102530002FEFE21AF20A0E946216C82F811107C01B +:102540008091CF019091D0010E94F415DFCF0E94BD +:10255000B5026C2F8091CF019091D0010E94B415EB +:10256000C701E11422E0F20659F3E1CF0E94611C99 +:102570008F929F92AF92BF92EF92FF920F931F9311 +:10258000CF93DF93CDB7DEB7A1970FB6F894DEBF38 +:102590000FBECDBF7C01FA01CB0119A2223008F495 +:1025A0002AE08E010F5D1F4F822E912CA12CB12CA1 +:1025B000BF01A50194010E941F1CF901CA0101502D +:1025C00011096A3014F4605D01C0695CD8016C9334 +:1025D000232B242B252B61F7B801C7010E94DF159F +:1025E000A1960FB6F894DEBF0FBECDBFDF91CF919D +:1025F0001F910F91FF90EF90BF90AF909F908F90A1 +:102600000895833081F028F4813099F08230A1F070 +:1026100008958730A9F08830B9F08430D1F48091E2 +:1026200080008F7D03C0809180008F778093800031 +:10263000089584B58F7702C084B58F7D84BD0895D9 +:102640008091B0008F7703C08091B0008F7D809320 +:10265000B00008952AE00C94B812CF92DF92EF9266 +:10266000FF920F931F93CF93DF9377FF1DC06A01F3 +:102670007B01EC016DE20E94B4158C0144275527C3 +:10268000BA014C195D096E097F092AE0CE010E944A +:10269000B812800F911FDF91CF911F910F91FF9082 +:1026A000EF90DF90CF9008952AE0DF91CF911F91B6 +:1026B0000F91FF90EF90DF90CF900C94B812462FBF +:1026C00050E060E070E02AE00C94B8120F931F9382 +:1026D000CF93DF93EC010E945F138C01CE010E9427 +:1026E000F415800F911FDF91CF911F910F910895E5 +:1026F000AB0160E070E02AE00C94B812CF93DF9356 +:10270000282F30E0F901EA57FC4F8491F901EE5887 +:10271000FC4FD491F901E25AFC4FC491CC2391F0C3 +:1027200081110E940113EC2FF0E0EE0FFF1FEC5A15 +:10273000FC4FA591B491EC91ED2381E090E021F460 +:1027400080E002C080E090E0DF91CF9108951F9378 +:10275000CF93DF93282F30E0F901EA57FC4F8491A3 +:10276000F901EE58FC4FD491F901E25AFC4FC491A3 +:10277000CC23C1F0162F81110E940113EC2FF0E041 +:10278000EE0FFF1FE65BFC4FA591B4919FB7F89445 +:10279000111104C08C91D095D82302C0EC91DE2B8E +:1027A000DC939FBFDF91CF911F910895CF93DF936B +:1027B00090E0FC01EE58FC4F2491FC01E25AFC4FE2 +:1027C0008491882361F190E0880F991FFC01E05CFF +:1027D000FC4FC591D491FC01E65BFC4FA591B491EF +:1027E000611109C09FB7F89488812095822388835E +:1027F000EC912E230BC0623061F49FB7F89488816E +:10280000322F309583238883EC912E2B2C939FBFFE +:1028100006C08FB7F894E8812E2B28838FBFDF91F5 +:10282000CF9108958E3008F08E5087708064809329 +:102830007C0080917A00806480937A0080917A0095 +:1028400086FDFCCF809178002091790090E0922B5A +:1028500008953FB7F8948091F4059091F505A09103 +:10286000F605B091F70526B5A89B05C02F3F19F0D6 +:102870000196A11DB11D3FBFBA2FA92F982F882700 +:10288000820F911DA11DB11DBC01CD0142E0660F5B +:10289000771F881F991F4A95D1F708952FB7F8948D +:1028A0006091F0057091F1058091F2059091F3052A +:1028B0002FBF08951F920F920FB60F9211242F93DE +:1028C0003F938F939F93AF93BF938091F005909127 +:1028D000F105A091F205B091F3053091EF0523E0E9 +:1028E000230F2D3720F40196A11DB11D05C026E848 +:1028F000230F0296A11DB11D2093EF058093F005D3 +:102900009093F105A093F205B093F3058091F4053F +:102910009091F505A091F605B091F7050196A11DDE +:10292000B11D8093F4059093F505A093F605B0933F +:10293000F705BF91AF919F918F913F912F910F908C +:102940000FBE0F901F901895CF92DF92EF92FF92DB +:102950000F931F93CF93DF936C017A018B01C0E03B +:10296000D0E0CE15DF0581F0D8016D918D01D60143 +:10297000ED91FC910190F081E02DC6010995892B24 +:1029800011F02196EECF7E01C701DF91CF911F910B +:102990000F91FF90EF90DF90CF900895089580E021 +:1029A00090E00895FC01538D448D252F30E0842F55 +:1029B00090E0821B930B541710F0CF960895019767 +:1029C0000895FC01918D828D981761F0828DDF0151 +:1029D000A80FB11D5D968C91928D9F5F9F73928F12 +:1029E00090E008958FEF9FEF0895FC01918D828D07 +:1029F000981731F0828DE80FF11D858D90E00895D4 +:102A00008FEF9FEF0895FC01918D228D892F90E02B +:102A1000805C9F4F821B91098F73992708958CE2E8 +:102A200095E00E94031521E0892B09F420E0822F14 +:102A30000895FC01848DDF01A80FB11DA35ABF4F7B +:102A40002C91848D90E001968F739927848FA689AD +:102A5000B7892C93A089B1898C91837080648C9301 +:102A6000938D848D981306C00288F389E02D8081B0 +:102A70008F7D80830895EF92FF920F931F93CF93E2 +:102A8000DF93EC0181E0888F9B8D8C8D981305C0BE +:102A9000E889F989808185FD26C0F62E0B8D10E02E +:102AA0000F5F1F4F0F731127E02E8C8DE8120CC0A3 +:102AB0000FB607FCFACFE889F989808185FFF5CF49 +:102AC000CE010E941915F1CF8B8DFE01E80FF11D8B +:102AD000E35AFF4FF0829FB7F8940B8FEA89FB8986 +:102AE000808180620AC09FB7F894EE89FF89608375 +:102AF000E889F98980818370806480839FBF81E049 +:102B000090E0DF91CF911F910F91FF90EF9008958A +:102B1000CF93DF93EC01888D8823C9F0EA89FB8984 +:102B2000808185FD05C0A889B9898C9186FD0FC07B +:102B30000FB607FCF5CF808185FFF2CFA889B98950 +:102B40008C9185FFEDCFCE010E941915E7CFDF9163 +:102B5000CF91089580E090E0892B29F00E940F1515 +:102B600081110C9400000895DC01ED91FC9101901D +:102B7000F081E02D0994EF92FF920F931F93CF9372 +:102B8000DF938C017B01C0E0D0E0F701EC0FFD1F6B +:102B90006491662361F0D801ED91FC910190F08180 +:102BA000E02DC8010995892B11F02196EECFCE01B9 +:102BB000DF91CF911F910F91FF90EF9008956115D4 +:102BC000710579F0FB0101900020E9F73197AF0121 +:102BD000461B570BDC01ED91FC910280F381E02D47 +:102BE000099480E090E0089564E571E00C94DF15AD +:102BF0000F931F93CF93DF93EC010E94BB158C01C1 +:102C0000CE010E94F415800F911FDF91CF911F918B +:102C10000F9108951F920F920FB60F9211242F93C8 +:102C20003F934F935F936F937F938F939F93AF9354 +:102C3000BF93EF93FF938CE295E00E941915FF91EB +:102C4000EF91BF91AF919F918F917F916F915F9124 +:102C50004F913F912F910F900FBE0F901F9018959D +:102C60001F920F920FB60F9211242F938F939F9361 +:102C7000EF93FF93E0913C05F0913D058081E09159 +:102C80004205F091430582FD12C090818091450577 +:102C90008F5F8F7320914605821751F0E0914505B3 +:102CA000F0E0E45DFA4F958F8093450501C0808187 +:102CB000FF91EF919F918F912F910F900FBE0F90E9 +:102CC0001F9018950C94E11BCF93DF93CDB7DEB71F +:102CD000A0970FB6F894DEBF0FBECDBF789484B531 +:102CE000826084BD84B5816084BD85B5826085BD08 +:102CF00085B5816085BD80916E00816080936E0096 +:102D00001092810080918100826080938100809187 +:102D10008100816080938100809180008160809338 +:102D200080008091B10084608093B1008091B000F8 +:102D300081608093B00080917A00846080937A00F3 +:102D400080917A00826080937A0080917A0081601D +:102D500080937A0080917A00806880937A00109244 +:102D6000C100E0913C05F0913D0582E08083E09157 +:102D70003805F09139051082E0913A05F0913B0554 +:102D800080E1808310924405E0914005F091410577 +:102D900086E08083E0913E05F0913F05808180616F +:102DA0008083E0913E05F0913F05808188608083BB +:102DB000E0913E05F0913F05808180688083E0913D +:102DC0003E05F0913F0580818F7D80830E9429140C +:102DD0004B015C01E4EFCE2EDD24D394E12CF12CE9 +:102DE0000E942914DC01CB0188199909AA09BB09A1 +:102DF000883E9340A105B10598F321E0C21AD1089D +:102E0000E108F10838EE830E33E0931EA11CB11CDB +:102E1000C114D104E104F10419F778948CE295E02F +:102E20000E94F4158CE295E09093D0018093CF013D +:102E30001092C7011092C6010E944E1460932305A0 +:102E40007093240580932505909326050E94E9112F +:102E50000E944802BE016F5F7F4F1B010E944E140B +:102E60006B017C018091230590912405A09125059B +:102E7000B0912605C816D906EA06FB0648F48091EB +:102E8000C6019091C70101969093C7018093C60136 +:102E9000C0922305D0922405E0922505F0922605E4 +:102EA0008091C501882311F18091C1019091C201E7 +:102EB000A091C301B091C40146015701881A990A33 +:102EC000AA0ABB0A21E882162EEE920626E3A20683 +:102ED000B10460F0C701B6010E94010DC092C101AA +:102EE000D092C201E092C301F092C4018CE295E05D +:102EF0000E940315892B31F08CE295E00E94E114C9 +:102F00000E94B5020E944E142B013C018091CB011E +:102F10009091CC01A091CD01B091CE01B301A2015D +:102F2000481B590B6A0B7B0B80910701909108019C +:102F3000A0E0B0E084179507A607B70708F04AC2DB +:102F40008091CA01823091F038F4882399F0813061 +:102F500009F036C282E0C2C0843009F4B0C08530C6 +:102F600009F02EC2D12C73E1C72E0DC1D12C63E123 +:102F7000C62E73C0F12C53E1E52E8091C901F816DD +:102F8000C0F4EF9C8001112407531A4FD801139607 +:102F9000EC91F0E0E55EFC4F849160E00E94A713A5 +:102FA00010922B05F8011286118614861386F3946D +:102FB000E4CF81E077C0CD9C8001112407531A4FE4 +:102FC000D8011296EC91F0E0E55EFC4F84910E94EE +:102FD00012147C01F801818592858E0D9F1D9287C8 +:102FE00081878091C8018330B8F168E173E08091F6 +:102FF000CF019091D0010E94BB150E944E14AB01ED +:10300000BC018091CF019091D0010E942A1362E10E +:1030100073E08091CF019091D0010E94BB15B70160 +:103020008091CF019091D0010E9478136FE073E0FE +:103030008091CF019091D0010E94BB1560912B052A +:103040008091CF019091D0010E945F138091CF01B8 +:103050009091D0010E94F415D394E090C901DE1440 +:1030600008F4A9CF80912B058F5F80932B05909159 +:103070000601891708F4AEC109EC15E0F12CEF1434 +:1030800071F0D8011396EC91F0E0E55EFC4F84916D +:1030900061E00E94A713F3940D5E1F4FF0CF1092D2 +:1030A0002B0584E08093CA010E944E14609327058B +:1030B000709328058093290590932A058BC18091F0 +:1030C000270590912805A0912905B0912A05481659 +:1030D00059066A067B0609F47DC185E08093CA0122 +:1030E00079C1CD9C8001112407531A4FD801129643 +:1030F000EC91F0E0E55EFC4F84910E9412147C019B +:10310000F801838594858E0D9F1D94878387809118 +:10311000C8018330B8F16CE073E08091CF019091E9 +:10312000D0010E94BB150E944E14AB01BC018091DE +:10313000CF019091D0010E942A1366E073E0809144 +:10314000CF019091D0010E94BB15B7018091CF01B2 +:103150009091D0010E94781363E073E08091CF01D9 +:103160009091D0010E94BB1560912B058091CF01F9 +:103170009091D0010E945F138091CF019091D00176 +:103180000E94F415D3948091C901D81608F4A9CFF0 +:1031900080912B058F5F80932B05909106018917F5 +:1031A00008F418C1C12C43E1D42E8091C901C8167E +:1031B00008F0FBC0EC2CF12CDE9CC001DF9C900DD4 +:1031C00011249C0127533A4F5901D9011396EC91D0 +:1031D000F0E0E55EFC4F849160E00E94A71320912F +:1031E000060130E0F50183859485B9010E94F71B43 +:1031F0008B0181859285B9010E94F71B061B170B75 +:1032000017FD96C20130F0E11F0714F000E010E155 +:10321000C801DE9CF001DF9CF00D1124E753FA4F4A +:103220002085468157814017510770F0DE9CF001E0 +:10323000DF9CF00D1124E753FA4F44815581841728 +:10324000950728F4912C04C09924939401C0922EE0 +:10325000DE9CF001DF9CF00D1124E753FA4FA584AA +:10326000B68439EEA3163DEFB306A8F4878590899E +:10327000A189B289A801012E000C660B770B480FBB +:10328000591F6A1F7B1F4787508B618B728BC5014B +:103290000196968785879216D1F1DE9CF001DF9C1E +:1032A000F00D1124E753FA4F2585368540E050E0B4 +:1032B00067857089818992890E941F1C213A6FE07D +:1032C00036074105510520F020EA3FE040E050E09C +:1032D000DE9CC001DF9C900D1124DC01A753BA4F86 +:1032E0005D01A901692D8D919C910E94E30DF5016D +:1032F0001786108A118A128A1686158680912205F1 +:10330000882329F0692D808191810E94670FDE9CBE +:10331000F001DF9CF00D1124E753FA4F9086809165 +:10332000C801823008F12091060130E01F930F930D +:1033300081859285B9010E94F71B7F936F938385E6 +:103340009485B9010E94F71B7F936F9382811F922E +:103350008F9332E3832E31E0932E9F928F923F9290 +:103360002F920E94661C0FC08823E9F01F930F93D1 +:1033700082811F928F9349E451E05F934F933F9274 +:103380002F920E94661C0091CF011091D001B101D3 +:10339000C8010E94DF15C8010E94F4150FB6F89409 +:1033A000DEBF0FBECDBFC39400CF1092CA014092C2 +:1033B000CB015092CC016092CD017092CE010AC037 +:1033C0001092CA0164EE72E08091CF019091D00119 +:1033D0000E94F81580912205882319F1F12C2DE225 +:1033E000E22E80912105F816E0F4EF9C8001112473 +:1033F0000B5F1C4FD80114968C91149781110FC04C +:10340000EC91F0E0E55EFC4F84910E947E13F801A0 +:1034100094A5891721F0682FC8010E94670FF394C3 +:10342000E0CF0E944E146B017C01809100039091CB +:103430000103A0910203B0910303B701A601481B49 +:10344000590B6A0B7B0B80910D0190910E01A0919D +:103450000F01B091100184179507A607B70700F177 +:10346000909121054091090150910A0160910B0151 +:1034700070910C01E5E0F3E080E0981709F4CAC010 +:1034800007A110A521A532A546015701801A910A6E +:10349000A20AB30ABD96481559056A057B0530F4A2 +:1034A000B12C8DE2682E9FE0A92E94C08F5FE5CFEE +:1034B0008B2C912C689C8001699C100D11240B5F52 +:1034C0001C4FA701960160E0C8010E94EC098091A1 +:1034D0000403882309F47DC08091FF0290E00296E6 +:1034E00064E170E00E940B1C9C8F8B8FD8017C9054 +:1034F0008091D1019091D2019C0140E050E02D8F4C +:103500003E8F4F8F58A300E010E0402E8B8D9C8D96 +:10351000800F911F64E170E00E940B1CA89EF001D7 +:10352000A99EF00D1124ED52FE4F8281781275C0D4 +:1035300080819181A0E0B0E04D8D5E8D6F8D78A18E +:10354000841B950BA60BB70B181619061A061B063B +:103550000CF063C06EE373E08091CF019091D001D5 +:103560000E94BB15672D8091CF019091D0010E94E0 +:103570005F136CE373E08091CF019091D0010E94C2 +:10358000BB15502E689CC001699C900D1124BC0194 +:10359000675F7C4F7E8F6D8F8B8D9C8D850D911D10 +:1035A00064E170E00E940B1C4C01A89EF001A99EF2 +:1035B000F00D1124ED52FE4F8281781689F053945C +:1035C000F4E15F12E9CF8091CF019091D0010E9488 +:1035D000F415B39480912105B81608F469CF12C090 +:1035E00080819181A0E0B0E02091D1013091D201A1 +:1035F000821B930BA109B109181619061A061B069E +:103600009CF0DDCFC0920003D0920103E092020350 +:10361000F09203030E94AA1521CC0F5F1F4F0431C3 +:10362000110509F072CFD5CF541441F069E373E06E +:103630008091CF019091D0010E94BB15A89C800180 +:10364000A99C100D11240D521E4FD8016D917C9133 +:103650008091CF019091D0010E94781367E373E0CD +:103660008091CF019091D0010E94BB15F801468155 +:103670005781608571854C195D096E097F098091BC +:10368000CF019091D0010E942D1365E373E08091EA +:10369000CF019091D0010E94BB15D8011A964D918F +:1036A0005D916D917C911D978091CF019091D0019A +:1036B0000E942A1363E373E08091CF019091D001BF +:1036C0000E94BB15F80163818091CF019091D001D8 +:1036D0000E945F13AD8DBE8D8C91882389F061E3CC +:1036E00073E08091CF019091D0010E94BB15F80149 +:1036F000648175818091CF019091D0010E947813EF +:10370000A89C8001A99C100D11240D521E4FD801B8 +:103710001E966C918091CF019091D0010E94B415BA +:10372000F801808191819093D2018093D10147CF9C +:10373000208580E090E000E010E078CDECE2F5E05C +:103740001382128288EE93E0A0E0B0E08483958338 +:10375000A683B78381E291E09183808385EC90E03A +:103760009587848784EC90E09787868780EC90E04B +:10377000918B808B81EC90E0938B828B82EC90E03C +:10378000958B848B86EC90E0978B868B118E128EB6 +:10379000138E148EE9ECF5E01286118614861386DA +:1037A000168615861786108A118A128A158E148E2F +:1037B000178E168E11A210A212A213A214A215A285 +:1037C0000895F999FECF92BD81BDF89A992780B5E9 +:1037D0000895262FF999FECF1FBA92BD81BD20BD55 +:1037E0000FB6F894FA9AF99A0FBE01960895AA1B9B +:1037F000BB1B51E107C0AA1FBB1FA617B70710F0DC +:10380000A61BB70B881F991F5A95A9F7809590950D +:10381000BC01CD01089597FB072E16F4009407D044 +:1038200077FD09D00E94F71B07FC05D03EF4909568 +:1038300081959F4F0895709561957F4F0895A1E2FE +:103840001A2EAA1BBB1BFD010DC0AA1FBB1FEE1F1A +:10385000FF1FA217B307E407F50720F0A21BB30B65 +:10386000E40BF50B661F771F881F991F1A9469F7E1 +:1038700060957095809590959B01AC01BD01CF013D +:103880000895EE0FFF1F0590F491E02D09940E941A +:10389000521CA59F900DB49F900DA49F800D911D6B +:1038A00011240895A29FB001B39FC001A39F700D82 +:1038B000811D1124911DB29F700D811D1124911D38 +:1038C000089581E090E0F8940C94501FAEE0B0E0D1 +:1038D000ECE6FCE10C94271F0D891E8986E08C83A1 +:1038E0001A8309838FEF9FE79E838D83AE01475E26 +:1038F0005F4F6F89788DCE0101960E94881CEF8101 +:10390000F885E00FF11F10822E96E4E00C94431F1F +:10391000ABE0B0E0EEE8FCE10C94191F6C017B0118 +:103920008A01FC0117821682838181FFCCC1CE01FE +:1039300001963C01F6019381F70193FD859193FF78 +:1039400081917F01882309F4BAC1853239F493FD4E +:10395000859193FF81917F01853229F4B60190E032 +:103960000E947F1EE7CF912C212C312CFFE1F31513 +:10397000D8F08B3279F038F4803279F08332A1F4C8 +:10398000232D20611DC08D3261F0803369F4232D19 +:10399000216016C0832D8260382EE32DE4603E2E18 +:1039A0002AC0F32DF8601DC037FC2DC020ED280F74 +:1039B0002A3040F08E32B9F436FC81C1232D2064C8 +:1039C000322E19C036FE06C08AE0989E200D1124C2 +:1039D000922E11C0EAE02E9E200D1124222EF32DEE +:1039E000F0623F2E08C08C3621F4832D8068382E7B +:1039F00002C0883641F4F70193FD859193FF8191D0 +:103A00007F018111B3CF982F9F7D9554933028F477 +:103A10000C5F1F4F9FE399830DC0833631F08337CE +:103A200071F0833509F059C021C0F8018081898384 +:103A30000E5F1F4F88248394912C530113C02801DB +:103A4000F2E04F0E511CF801A080B18036FE03C099 +:103A5000692D70E002C06FEF7FEFC5010E94741EF8 +:103A60004C018201F32DFF773F2E16C0280122E082 +:103A7000420E511CF801A080B18036FE03C0692DB2 +:103A800070E002C06FEF7FEFC5010E94691E4C011C +:103A9000F32DF0683F2E820133FC1BC0822D90E095 +:103AA00088169906B0F4B60180E290E00E947F1E6D +:103AB0002A94F4CFF50137FC859137FE81915F019F +:103AC000B60190E00E947F1E21102A9421E0821A04 +:103AD00091088114910471F7E8C0843611F0893699 +:103AE00041F5F80137FE07C06081718182819381C1 +:103AF0000C5F1F4F08C060817181072E000C880B7E +:103B0000990B0E5F1F4FF32DFF763F2E97FF09C0D5 +:103B100090958095709561957F4F8F4F9F4FF0687E +:103B20003F2E2AE030E0A3010E94BB1E882E86189B +:103B300045C0853731F4232D2F7EB22E2AE030E0A8 +:103B400025C0932D997FB92E8F36C1F018F4883592 +:103B500079F0B5C0803719F0883721F0B0C0E92F6F +:103B6000E061BE2EB4FE0DC0FB2DF460BF2E09C077 +:103B700034FE0AC0292F2660B22E06C028E030E0AD +:103B800005C020E130E002C020E132E0F801B7FEDC +:103B900007C060817181828193810C5F1F4F06C0D5 +:103BA0006081718180E090E00E5F1F4FA3010E9451 +:103BB000BB1E882E8618FB2DFF773F2E36FE0DC0CC +:103BC000232D2E7FA22E891458F434FE0BC032FC14 +:103BD00009C0832D8E7EA82E05C0B82CA32C03C04F +:103BE000B82C01C0B92CA4FE0FC0FE01E80DF11DD8 +:103BF0008081803321F49A2D997EA92E09C0A2FEDE +:103C000006C0B394B39404C08A2D867809F0B394A7 +:103C1000A3FC11C0A0FE06C0B21488F4280C922C9C +:103C20009B180EC0B21460F4B60180E290E00E94CE +:103C30007F1EB394F7CFB21418F42B1802C0982C3F +:103C4000212CA4FE10C0B60180E390E00E947F1EEC +:103C5000A2FE17C0A1FC03C088E790E002C088E57F +:103C600090E0B6010CC08A2D867859F0A1FE02C002 +:103C70008BE201C080E2A7FC8DE2B60190E00E94D9 +:103C80007F1E891438F4B60180E390E00E947F1E05 +:103C90009A94F7CF8A94F301E80DF11D8081B60163 +:103CA00090E00E947F1E8110F5CF222009F442CEC1 +:103CB000B60180E290E00E947F1E2A94F6CFF601C2 +:103CC0008681978102C08FEF9FEF2B96E2E10C94E3 +:103CD000351FFC010590615070400110D8F78095A8 +:103CE00090958E0F9F1F0895FC01615070400190C8 +:103CF0000110D8F7809590958E0F9F1F08950F9310 +:103D00001F93CF93DF93FB01238121FD03C08FEF2E +:103D10009FEF2CC022FF16C0468157812481358138 +:103D20004217530744F4A081B1819D012F5F3F4F9B +:103D3000318320838C93268137812F5F3F4F3783D8 +:103D4000268314C08B01EC01FB010084F185E02D7A +:103D50000995892BE1F6D80116968D919C911797BC +:103D6000019617969C938E931697CE01DF91CF9173 +:103D70001F910F910895FA01AA27283051F120319F +:103D800081F1E8946F936E7F6E5F7F4F8F4F9F4FEF +:103D9000AF4FB1E03ED0B4E03CD0670F781F891F31 +:103DA0009A1FA11D680F791F8A1F911DA11D6A0FFF +:103DB000711D811D911DA11D20D009F468943F91B2 +:103DC0002AE0269F11243019305D3193DEF6CF01B1 +:103DD0000895462F4770405D4193B3E00FD0C9F777 +:103DE000F6CF462F4F70405D4A3318F0495D31FDE4 +:103DF0004052419302D0A9F7EACFB4E0A695979537 +:103E0000879577956795BA95C9F70097610571050C +:103E100008959B01AC010A2E069457954795379556 +:103E20002795BA95C9F7620F731F841F951FA01DB0 +:103E300008952F923F924F925F926F927F928F924E +:103E40009F92AF92BF92CF92DF92EF92FF920F9329 +:103E50001F93CF93DF93CDB7DEB7CA1BDB0B0FB633 +:103E6000F894DEBF0FBECDBF09942A8839884888F0 +:103E70005F846E847D848C849B84AA84B984C88486 +:103E8000DF80EE80FD800C811B81AA81B981CE0F7D +:103E9000D11D0FB6F894DEBF0FBECDBFED01089562 +:043EA000F894FFCFC4 +:103EA4000200D007000004320060EA000030750010 +:103EB400000D1307080E006B006C006D000000007D +:103EC400003B15A414D21488150315E114F5142528 +:103ED40075004C2532643A202534642C2025346442 +:103EE400202D3E2025203464004C2532643A2025C0 +:083EF400203464000D0A0000F7 +:00000001FF