mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-01-31 06:39:11 +00:00
8d63c3e5b6
git-svn-id: https://svn.fhem.de/fhem/trunk@26790 2b470e98-0d58-463d-a4d8-8e2adae1ed80
2999 lines
139 KiB
Perl
Executable File
2999 lines
139 KiB
Perl
Executable File
#############################################################################
|
|
# $Id$
|
|
# fhem Modul für Impulszähler auf Basis von Arduino mit ArduCounter Sketch
|
|
#
|
|
# This file is part of fhem.
|
|
#
|
|
# Fhem is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Fhem is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with fhem. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
#
|
|
#
|
|
# ideas / todo:
|
|
# - check integration of device none and write tests
|
|
# - DevIo_IsOpen instead of checking fd
|
|
# - "static" ports that do not count but report every change or an analog value to Fhem
|
|
# - 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 thresholds automatically for analog input, track drift
|
|
# - timeMissed
|
|
# - rename openRetries to helloRetries
|
|
#
|
|
|
|
#
|
|
# 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 ArduCounter;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use GPUtils qw(:all);
|
|
use Time::HiRes qw(gettimeofday time);
|
|
use DevIo;
|
|
use FHEM::HTTPMOD::Utils qw(:all);
|
|
|
|
use Exporter ('import');
|
|
our @EXPORT_OK = qw();
|
|
our %EXPORT_TAGS = (all => [@EXPORT_OK]);
|
|
|
|
BEGIN {
|
|
GP_Import( qw(
|
|
fhem
|
|
CommandAttr
|
|
CommandDeleteAttr
|
|
addToDevAttrList
|
|
AttrVal
|
|
ReadingsVal
|
|
ReadingsTimestamp
|
|
readingsSingleUpdate
|
|
readingsBeginUpdate
|
|
readingsBulkUpdate
|
|
readingsEndUpdate
|
|
readingsDelete
|
|
InternalVal
|
|
makeReadingName
|
|
Log3
|
|
RemoveInternalTimer
|
|
InternalTimer
|
|
deviceEvents
|
|
EvalSpecials
|
|
AnalyzePerlCommand
|
|
CheckRegexp
|
|
IsDisabled
|
|
devspec2array
|
|
FmtTime
|
|
gettimeofday
|
|
FmtDateTime
|
|
GetTimeSpec
|
|
fhemTimeLocal
|
|
time_str2num
|
|
min
|
|
max
|
|
minNum
|
|
maxNum
|
|
abstime2rel
|
|
defInfo
|
|
trim
|
|
ltrim
|
|
rtrim
|
|
UntoggleDirect
|
|
UntoggleIndirect
|
|
IsInt
|
|
fhemNc
|
|
round
|
|
sortTopicNum
|
|
Svn_GetFile
|
|
WriteFile
|
|
|
|
DevIo_OpenDev
|
|
DevIo_SimpleWrite
|
|
DevIo_SimpleRead
|
|
DevIo_CloseDev
|
|
DevIo_Disconnected
|
|
DevIo_setStates
|
|
DevIo_getState
|
|
SetExtensions
|
|
HttpUtils_NonblockingGet
|
|
|
|
featurelevel
|
|
defs
|
|
modules
|
|
attr
|
|
init_done
|
|
));
|
|
|
|
GP_Export( qw(
|
|
Initialize
|
|
));
|
|
};
|
|
|
|
|
|
my $Module_version = '8.01 - 04.12.2022';
|
|
|
|
|
|
my %SetHash = (
|
|
'disable' => '',
|
|
'enable' => '',
|
|
'raw' => '',
|
|
'reset' => '',
|
|
'resetWifi' => '',
|
|
'flash' => '',
|
|
'saveConfig' => '',
|
|
'clearLevels' => '',
|
|
'counter' => '',
|
|
'clearCounters' => '',
|
|
'clearHistory' => '',
|
|
'reconnect' => ''
|
|
);
|
|
|
|
|
|
my %GetHash = (
|
|
'info' => '',
|
|
'history' => '',
|
|
'levels' => ''
|
|
);
|
|
|
|
|
|
my %AnalogPinMap = (
|
|
'NANO' => {
|
|
'A0' => 14,
|
|
'A1' => 15,
|
|
'A2' => 16,
|
|
'A3' => 17,
|
|
'A4' => 18,
|
|
'A5' => 19,
|
|
'A6' => 20,
|
|
'A7' => 21 },
|
|
'ESP8266' => {
|
|
'A0' => 17 },
|
|
'ESP32' => {
|
|
'A0' => 36 },
|
|
'T-Display' => {
|
|
'A0' => 36 }
|
|
);
|
|
my %rAnalogPinMap;
|
|
|
|
|
|
|
|
#########################################################################
|
|
# FHEM module intitialisation
|
|
# defines the functions to be called from FHEM
|
|
sub Initialize {
|
|
my $hash = shift;
|
|
|
|
$hash->{ReadFn} = \&ArduCounter::ReadFn;
|
|
$hash->{ReadyFn} = \&ArduCounter::ReadyFn;
|
|
$hash->{DefFn} = \&ArduCounter::DefineFn;
|
|
$hash->{UndefFn} = \&ArduCounter::UndefFn;
|
|
$hash->{GetFn} = \&ArduCounter::GetFn;
|
|
$hash->{SetFn} = \&ArduCounter::SetFn;
|
|
$hash->{AttrFn} = \&ArduCounter::AttrFn;
|
|
$hash->{NotifyFn} = \&ArduCounter::NotifyFn;
|
|
$hash->{AttrList} =
|
|
'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 ' . # 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
|
|
'readingNameInterpolatedCount[AD]?[0-9]+ ' . # long term count including interpolation for offline times
|
|
'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 ' .
|
|
'configDelay ' . # how many seconds to wait before sending config after reboot of board
|
|
'keepAliveDelay ' .
|
|
'keepAliveTimeout ' .
|
|
'keepAliveRetries ' .
|
|
'nextOpenDelay ' .
|
|
'silentReconnect:0,1 ' .
|
|
'openTimeout ' .
|
|
'maxHist ' .
|
|
'deviceDisplay ' .
|
|
'logFilter ' .
|
|
'disable:0,1 ' .
|
|
'do_not_notify:1,0 ' .
|
|
$main::readingFnAttributes;
|
|
|
|
# initialize rAnalogPinMap for each board and pin
|
|
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";
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
##########################################################################
|
|
# Define command
|
|
sub DefineFn {
|
|
my $hash = shift; # reference to the Fhem device hash
|
|
my $def = shift; # definition string
|
|
my @a = split( /[ \t]+/, $def ); # the above string split at space or tab
|
|
|
|
return 'wrong syntax: define <name> ArduCounter devicename@speed or ipAdr:port'
|
|
if ( @a < 3 );
|
|
|
|
$hash->{devioNoSTATE} = 1; # let stateFormat do its thing, just set the reading 'state', not the Internal STATE
|
|
DevIo_CloseDev($hash); # make sure it is initially closed
|
|
DevIo_setStates($hash, 'disconnected'); # initial value, no triggers
|
|
my $name = $a[0];
|
|
my $dev = $a[2];
|
|
|
|
if ($dev =~ m/^[Nn]one$/) { # none
|
|
# for testing
|
|
} elsif ($dev =~ m/^(.+):([0-9]+)$/) { # tcp conection with explicit port
|
|
$hash->{TCP} = 1;
|
|
}
|
|
elsif ($dev =~ m/^(\d+\.\d+\.\d+\.\d+)(?:\:([0-9]+))?$/) {
|
|
$hash->{TCP} = 1;
|
|
$dev .= ':80' if (!$2); # ip adr with optional port
|
|
}
|
|
else { # serial connection
|
|
if ($dev !~ /.+@([0-9]+)/) {
|
|
$dev .= '@115200'; # add new default serial speed
|
|
} else {
|
|
Log3 $name, 3, "$name: Warning: connection speed $1 is not the default for the latest ArduCounter firmware"
|
|
if ($1 != 115200);
|
|
}
|
|
}
|
|
$hash->{DeviceName} = $dev;
|
|
$hash->{VersionModule} = $Module_version;
|
|
$hash->{NOTIFYDEV} = 'global'; # NotifyFn nur aufrufen wenn global events (INITIALIZED)
|
|
|
|
delete $hash->{Initialized}; # device might not be initialized - wait for hello / setup before cmds
|
|
|
|
Log3 $name, 3, "$name: defined with $dev, Module version $Module_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
|
|
sub UndefFn {
|
|
my $hash = shift;
|
|
DevIo_CloseDev($hash);
|
|
return;
|
|
}
|
|
|
|
|
|
#####################################################
|
|
# remove timers, call DevIo_Disconnected
|
|
# to set state and add to readyFnList
|
|
sub SetDisconnected {
|
|
my $hash = shift;
|
|
my $name = $hash->{NAME};
|
|
|
|
RemoveInternalTimer ("alive:$name"); # no timeout if waiting for keepalive response
|
|
RemoveInternalTimer ("keepAlive:$name"); # don't send keepalive messages anymore
|
|
RemoveInternalTimer ("sendHello:$name");
|
|
DevIo_Disconnected($hash); # close, set state and add to readyFnList so _Ready is called to reopen
|
|
return;
|
|
}
|
|
|
|
|
|
#####################################################
|
|
# open callback
|
|
sub OpenCallback {
|
|
my $hash = shift;
|
|
my $msg = shift;
|
|
my $name = $hash->{NAME};
|
|
my $now = gettimeofday();
|
|
if ($msg) {
|
|
Log3 $name, 5, "$name: Open callback: $msg" if ($msg);
|
|
}
|
|
delete $hash->{BUSY_OPENDEV};
|
|
if ($hash->{FD}) {
|
|
Log3 $name, 5, "$name: DoOpen succeeded in callback";
|
|
DevIo_setStates($hash, 'opened');
|
|
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);
|
|
|
|
if ($hash->{TCP}) {
|
|
# send first keepalive immediately to turn on tcp mode in device
|
|
KeepAlive("keepAlive:$name");
|
|
}
|
|
}
|
|
else { # no file descriptor after open
|
|
#Log3 $name, 5, "$name: DoOpen failed - open callback called from DevIO without FD";
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
##########################################################################
|
|
# Open Device
|
|
# called from Notify after init_done or when a new device is defined later,
|
|
# or from Ready as reopen,
|
|
# from attr when disable is removed / set to 0,
|
|
# from set reconnect, reset or after flash,
|
|
# from delayed_open when a tcp connection was closed with "already busy"
|
|
#
|
|
# normally an open also resets the counter board, unless its hardware is modified
|
|
# to continue when opened.
|
|
#
|
|
sub DoOpen {
|
|
my $hash = shift;
|
|
my $reopen = shift // 0;
|
|
my $name = $hash->{NAME};
|
|
my $now = gettimeofday();
|
|
my $caller = FhemCaller();
|
|
|
|
if ($hash->{DeviceName} eq 'none') {
|
|
Log3 $name, 5, "$name: open called from $caller, device is defined with none" if ($caller ne 'Ready');
|
|
SetStates($hash, 'opened');
|
|
return;
|
|
}
|
|
|
|
if ($hash->{BUSY_OPENDEV}) { # still waiting for callback to last open
|
|
if ($hash->{LASTOPEN} && $now > $hash->{LASTOPEN} + (AttrVal($name, "openTimeout", 3) * 2)
|
|
&& $now > $hash->{LASTOPEN} + 15) {
|
|
Log3 $name, 5, "$name: _Open - still waiting for open callback, timeout is over twice - this should never happen";
|
|
Log3 $name, 5, "$name: _Open - stop waiting and reset the flag.";
|
|
$hash->{BUSY_OPENDEV} = 0;
|
|
}
|
|
else { # no timeout yet
|
|
Log3 $name, 5, "$name: _Open - still waiting for open callback";
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!$reopen) { # not called from _Ready
|
|
DevIo_CloseDev($hash); # doesn't change state reading
|
|
DevIo_setStates($hash, 'disconnected'); # initial value, no triggers
|
|
delete $hash->{NEXT_OPEN};
|
|
delete $hash->{DevIoJustClosed};
|
|
}
|
|
|
|
Log3 $name, 4, "$name: trying to open connection to $hash->{DeviceName}" if (!$reopen);
|
|
|
|
$hash->{BUSY_OPENDEV} = 1;
|
|
$hash->{LASTOPEN} = $now;
|
|
$hash->{nextOpenDelay} = AttrVal($name, "nextOpenDelay", 60);
|
|
$hash->{devioLoglevel} = (AttrVal($name, "silentReconnect", 0) ? 4 : 3);
|
|
$hash->{TIMEOUT} = AttrVal($name, "openTimeout", 3);
|
|
$hash->{buffer} = ""; # clear Buffer for reception
|
|
|
|
DevIo_OpenDev($hash, $reopen, 0, \&OpenCallback);
|
|
delete $hash->{TIMEOUT};
|
|
if ($hash->{FD}) {
|
|
Log3 $name, 5, "$name: DoOpen succeeded immediately" if (!$reopen);
|
|
DevIo_setStates($hash, 'opened');
|
|
} else {
|
|
Log3 $name, 5, "$name: DoOpen waiting for callback" if (!$reopen);
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
#################################################################
|
|
# set state Reading and STATE internal
|
|
# call instead of setting STATE directly and when inactive / disconnected
|
|
sub SetStates {
|
|
my $hash = shift;
|
|
my $state = shift;
|
|
my $name = $hash->{NAME};
|
|
my $newState = $state;
|
|
|
|
Log3 $name, 5, "$name: SetState called from " . FhemCaller() . " with $state sets state and STATE to $newState";
|
|
#$hash->{STATE} = $newState; # leave to be set via stateFormat when reading 'state' changes
|
|
return if ($newState eq ReadingsVal($name, 'state', ''));
|
|
readingsSingleUpdate($hash, 'state', $newState, 1);
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
#########################################################################
|
|
sub ReadyFn {
|
|
my $hash = shift;
|
|
my $name = $hash->{NAME};
|
|
|
|
if(DevIo_getState($hash) eq 'disconnected') {
|
|
RemoveInternalTimer ("alive:$name"); # no timeout if waiting for keepalive response
|
|
RemoveInternalTimer ("keepAlive:$name"); # don't send keepalive messages anymore
|
|
delete $hash->{Initialized}; # when reconnecting wait for setup / hello before further action
|
|
if (IsDisabled($name)) {
|
|
Log3 $name, 3, "$name: _Ready: $name is disabled - don't try to reconnect";
|
|
DevIo_CloseDev($hash); # close, remove from readyfnlist so _ready is not called again
|
|
DevIo_setStates($hash, 'disconnected'); # no triggers
|
|
return;
|
|
}
|
|
DoOpen($hash, 1); # reopen, don't call DevIoClose before reopening
|
|
return; # a return value triggers direct read for win
|
|
}
|
|
# This is relevant for windows/USB only
|
|
my $po = $hash->{USBDev};
|
|
if ($po) {
|
|
my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status;
|
|
return ($InBytes>0); # tell fhem.pl to read when we return
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
#######################################################
|
|
# called from InternalTimer
|
|
# if TCP connection is busy or after firmware flash
|
|
sub DelayedOpen {
|
|
my $param = shift;
|
|
my (undef,$name) = split(/:/,$param);
|
|
my $hash = $defs{$name};
|
|
|
|
Log3 $name, 4, "$name: try to reopen connection after delay";
|
|
RemoveInternalTimer ("delayedopen:$name");
|
|
delete $hash->{DevIoJustClosed}; # otherwise open returns without doing anything this time and we are not on the readyFnList ...
|
|
DoOpen($hash, 1); # reopen
|
|
return;
|
|
}
|
|
|
|
|
|
########################################################
|
|
# Notify for INITIALIZED or Modified
|
|
# -> Open connection to device
|
|
sub NotifyFn {
|
|
my ($hash, $source) = @_;
|
|
return if($source->{NAME} ne "global");
|
|
|
|
my $events = deviceEvents($source, 1);
|
|
return if(!$events);
|
|
|
|
my $name = $hash->{NAME};
|
|
# Log3 $name, 5, "$name: Notify called for source $source->{NAME} with events: @{$events}";
|
|
|
|
return if (!grep {m/^INITIALIZED|REREADCFG|(MODIFIED $name)$|(DEFINED $name)$/} @{$events});
|
|
# DEFINED is not triggered if init is not done.
|
|
|
|
if (IsDisabled($name)) {
|
|
Log3 $name, 3, "$name: Notify / Init: device is disabled";
|
|
return;
|
|
}
|
|
|
|
Log3 $name, 3, "$name: Notify called with events: @{$events}, " .
|
|
"open device and set timer to send hello to device";
|
|
DoOpen($hash);
|
|
return;
|
|
}
|
|
|
|
|
|
######################################
|
|
# wrapper for DevIo write
|
|
sub DoWrite {
|
|
my $hash = shift;
|
|
my $line = shift;
|
|
my $name = $hash->{NAME};
|
|
if (!IsOpen($hash)) {
|
|
Log3 $name, 5, "$name: Write: device is disconnected, dropping line to write";
|
|
return 0;
|
|
}
|
|
if (IsDisabled($name)) {
|
|
Log3 $name, 5, "$name: Write called but device is disabled, dropping line to send";
|
|
return 0;
|
|
}
|
|
#Log3 $name, 5, "$name: Write: $line"; # devio will already log the write
|
|
#DevIo_SimpleWrite($hash, "\n", 2);
|
|
if ($hash->{DeviceName} eq 'none') {
|
|
Log3 $name, 4, "$name: Simulate sending to none: $line";
|
|
} else {
|
|
DevIo_SimpleWrite($hash, "$line.", 2);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
|
|
########################################################################################
|
|
# 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 InternalPinNumber {
|
|
my $hash = shift;
|
|
my $pinName = shift;
|
|
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 " . FhemCaller() .
|
|
" 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 " . FhemCaller() .
|
|
" can not determine internal pin number for $pinName." .
|
|
" No known board seems to support it";
|
|
}
|
|
else {
|
|
Log3 $name, 3, "$name: PinNumber called from " . FhemCaller() .
|
|
" 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 " . FhemCaller() .
|
|
" returns $pin for $pinName";
|
|
}
|
|
else {
|
|
Log3 $name, 5, "$name: PinNumber called from " . FhemCaller() .
|
|
" 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 PinName {
|
|
my $hash = shift;
|
|
my $pin = shift;
|
|
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 AttrValFromList {
|
|
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 (" . FhemCaller() . ") $a1 not there";
|
|
return AttrVal($name, $a2, undef) if (defined ($a2) && defined (AttrVal($name, $a2, undef)));
|
|
#Log3 $name, 5, "$name: AAV (" . FhemCaller() . ") $a2 not there";
|
|
return AttrVal($name, $a3, undef) if (defined ($a3) && defined (AttrVal($name, $a3, undef)));
|
|
#Log3 $name, 5, "$name: AAV (" . FhemCaller() . ") $a3 not there";
|
|
return AttrVal($name, $a4, undef) if (defined ($a4) && defined (AttrVal($name, $a4, undef)));
|
|
#Log3 $name, 5, "$name: AAV (" . FhemCaller() . ") $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 LogPinDesc {
|
|
my $hash = shift;
|
|
my $pin = shift;
|
|
my $pinName = PinName ($hash, $pin);
|
|
return AttrValFromList($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 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; # accept value but don't send it to the device yet.
|
|
}
|
|
my ($pin, $pinName) = 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, 5, "$name: ConfigurePin creates command ${pin},${opt}a";
|
|
DoWrite($hash, "${pin},${opt}a"); # initialized is already checked above
|
|
return;
|
|
}
|
|
|
|
|
|
######################################################
|
|
# send 'i' command to the device to configure a pin
|
|
sub ConfigureIntervals {
|
|
my $hash = shift;
|
|
my $aVal = shift;
|
|
my $name = $hash->{NAME};
|
|
my $cmd;
|
|
|
|
if (!defined($aVal)) {
|
|
$aVal = AttrVal($name, "interval", "");
|
|
if (!$aVal) {
|
|
Log3 $name, 4, "$name: attr interval not set";
|
|
return;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
$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 { # old firmware
|
|
$cmd = "${min},${max},${sml},${cnt}i";
|
|
}
|
|
Log3 $name, 5, "$name: ConfigureIntervals creates command $cmd";
|
|
DoWrite($hash, $cmd);
|
|
return;
|
|
}
|
|
|
|
|
|
######################################################
|
|
# send 'a' command to the device to configure a pin
|
|
sub 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; # accept value but don't send it to the device yet.
|
|
}
|
|
|
|
my $cmd = "${eHist},${eSerial},${pinDebug},${aDebug},${eTime}v";
|
|
Log3 $name, 5, "$name: ConfigureVerboseLevels creates command $cmd";
|
|
DoWrite($hash, $cmd);
|
|
return;
|
|
}
|
|
|
|
|
|
######################################################
|
|
# encode string as int sequence
|
|
# used in communication with device
|
|
sub 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 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; # accept value but don't send it to the device yet.
|
|
}
|
|
($pin, $pinName) = ParsePin($hash, $pinArg);
|
|
return "illegal pin $pinArg" if (!defined($pin)); # parsePin logs error if wrong pin spec
|
|
|
|
$ppu = AttrValFromList($hash, 0, "readingPulsesPerUnit$pinName", "readingPulsesPerUnit$pin");
|
|
$ppu = AttrValFromList($hash, 0, "readingPulsesPerKWh$pinName", "readingPulsesPerKWh$pin") if (!$ppu);
|
|
$ppu = AttrValFromList($hash, 1, "pulsesPerUnit", "pulsesPerKWh") if (!$ppu);
|
|
$fut = AttrValFromList($hash, 60, "readingFlowUnitTime$pinName", "readingFlowUnitTime$pin");
|
|
Log3 $name, 5, "$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, 5, "$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; # accept value but don't send it to the device yet.
|
|
}
|
|
my $cmd = "$pin,$ppu,$fDiv" . IntString($unit) . ",$fut" . IntString($funit) . "u";
|
|
Log3 $name, 5, "$name: ConfigureDisplay creates command $cmd";
|
|
DoWrite($hash, $cmd);
|
|
return;
|
|
}
|
|
|
|
|
|
#######################################################
|
|
# called from InternalTimer
|
|
# if relevant attr is changed
|
|
sub DelayedConfigureDisplay {
|
|
my $param = shift;
|
|
my (undef,$name) = split(/:/,$param);
|
|
my $hash = $defs{$name};
|
|
|
|
Log3 $name, 5, "$name: call configureDisplay after delay";
|
|
RemoveInternalTimer ("delayedcdisp:$name");
|
|
ConfigureDisplay($hash);
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
#######################################
|
|
# Aufruf aus InternalTimer
|
|
# send "h" to ask for "Hello" since device didn't say "Started" so far - maybe it's still counting ...
|
|
# called with timer from _openCB, _Ready and if count is read in _Parse but no hello was received
|
|
sub AskForHello {
|
|
my $param = shift;
|
|
my (undef,$name) = split(/:/,$param);
|
|
my $hash = $defs{$name};
|
|
|
|
Log3 $name, 5, "$name: ArduCounter $Module_version sending h(ello) to device to ask for firmware version";
|
|
return if (!DoWrite( $hash, "h"));
|
|
|
|
my $now = gettimeofday();
|
|
my $hwt = AttrVal($name, "helloWaitTime", 2);
|
|
RemoveInternalTimer ("hwait:$name");
|
|
InternalTimer($now+$hwt, "ArduCounter::HelloTimeout", "hwait:$name", 0);
|
|
$hash->{WaitForHello} = 1;
|
|
return;
|
|
}
|
|
|
|
|
|
#######################################
|
|
# Aufruf aus InternalTimer
|
|
sub HelloTimeout {
|
|
my $param = shift;
|
|
my (undef,$name) = split(/:/,$param);
|
|
my $hash = $defs{$name};
|
|
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}";
|
|
DoOpen($hash); # try again
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
############################################
|
|
# Aufruf aus Open / Ready und InternalTimer
|
|
# send "1k" to ask for "alive"
|
|
sub KeepAlive {
|
|
my $param = shift;
|
|
my (undef,$name) = split(/:/,$param);
|
|
my $hash = $defs{$name};
|
|
my $now = gettimeofday();
|
|
|
|
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" if (AttrVal($name, "logFilter", "N") =~ "N");
|
|
DoWrite( $hash, "1,${kdl}k");
|
|
RemoveInternalTimer ("alive:$name");
|
|
InternalTimer($now+$kto, "ArduCounter::AliveTimeout", "alive:$name", 0);
|
|
#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
|
|
#Log3 $name, 5, "$name: keepAlive timer for next message set in $kdl";
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
#######################################
|
|
# Aufruf aus InternalTimer
|
|
sub AliveTimeout {
|
|
my $param = shift;
|
|
my (undef,$name) = split(/:/,$param);
|
|
my $hash = $defs{$name};
|
|
#Log3 $name, 5, "$name: AliveTimeout called";
|
|
$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";
|
|
SetDisconnected($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}";
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
##########################################################################
|
|
# Send config commands after Board reported it is ready or still counting
|
|
# called when parsing hello message from device
|
|
sub ConfigureDevice {
|
|
my $param = shift;
|
|
my (undef,$name) = split(/:/,$param);
|
|
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";
|
|
my @attrPins = sort grep {/pin([dDaA])?[\d]/} keys %{$attr{$name}};
|
|
#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]";
|
|
}
|
|
Log3 $name, 5, "$name: ConfigureDevice: send config";
|
|
while (my ($aName, $val) = each(%{$attr{$name}})) {
|
|
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 = InternalPinNumber($hash, $pinName) if ($type && $type eq 'A'); # if this is an analog pin specification translate it
|
|
if ($aPinNum) {
|
|
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) { # 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) {
|
|
Log3 $name, 5, "$name: ConfigureDevice: removing pin $pin";
|
|
DoWrite($hash, "${pin}d");
|
|
}
|
|
}
|
|
else {
|
|
Log3 $name, 5, "$name: ConfigureDevice: no pins in running config without attribute in Fhem";
|
|
}
|
|
|
|
ConfigureIntervals($hash);
|
|
ConfigureVerboseLevels($hash);
|
|
ConfigureDisplay($hash) if ($hash->{Board} =~ /Display/);
|
|
|
|
DoWrite( $hash, "s"); # get new running config
|
|
return;
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
# Attr command
|
|
sub AttrFn {
|
|
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 @_";
|
|
if ($cmd eq "set") {
|
|
if ($aName =~ /^pin([DA]?\d+)/) { # pin attribute -> add a pin
|
|
my $pinName = $1;
|
|
return ConfigurePin($hash, $pinName, $aVal);
|
|
}
|
|
elsif ($aName eq "devVerbose") {
|
|
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 ConfigureVerboseLevels($hash, $aVal);
|
|
}
|
|
elsif ($aName eq "enableSerialEcho") {
|
|
return ConfigureVerboseLevels($hash, undef, $aVal);
|
|
}
|
|
elsif ($aName eq "enablePinDebug") {
|
|
return ConfigureVerboseLevels($hash, undef, undef, $aVal);
|
|
}
|
|
elsif ($aName eq "enableAnalogDebug") {
|
|
return ConfigureVerboseLevels($hash, undef, undef, undef, $aVal);
|
|
}
|
|
elsif ($aName eq "enableDevTime") {
|
|
return 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;
|
|
if ($min < 1 || $min > 1023 || $max < $min || $max > 1023) {
|
|
Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal";
|
|
return "Invalid Value $aVal";
|
|
}
|
|
if ($hash->{Initialized}) {
|
|
DoWrite($hash, "${min},${max}t");
|
|
} 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 "interval") {
|
|
return 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";
|
|
return "Invalid Value $aVal";
|
|
}
|
|
}
|
|
elsif ($aName eq "keepAliveDelay") {
|
|
if ($aVal =~ '^(\d+)$') {
|
|
if ($aVal > 3600) {
|
|
Log3 $name, 3, "$name: value too big in attr $name $aName $aVal";
|
|
return "Value too big: $aVal";
|
|
}
|
|
# todo: set new keepalive delay and communicate to device
|
|
}
|
|
else {
|
|
Log3 $name, 3, "$name: Invalid value in attr $name $aName $aVal";
|
|
return "Invalid Value $aVal";
|
|
}
|
|
}
|
|
elsif ($aName eq 'disable') {
|
|
if ($aVal) {
|
|
Log3 $name, 5, "$name: disable attribute set";
|
|
if ($hash->{DeviceName} eq 'none') {
|
|
Log3 $name, 4, "$name: Simulate closing connection to none";
|
|
}
|
|
else {
|
|
DevIo_CloseDev($hash); # really close and remove from readyFnList again
|
|
DevIo_setStates($hash, 'disconnected'); # initial value, no triggers
|
|
RemoveInternalTimer ("alive:$name"); # no timeout if waiting for keepalive response
|
|
RemoveInternalTimer ("keepAlive:$name"); # don't send keepalive messages anymore
|
|
RemoveInternalTimer ("sendHello:$name");
|
|
}
|
|
return;
|
|
} else {
|
|
Log3 $name, 3, "$name: disable attribute cleared";
|
|
DoOpen($hash) if ($init_done); # only if fhem is initialized
|
|
}
|
|
}
|
|
elsif ($aName eq 'deviceDisplay') {
|
|
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.
|
|
}
|
|
my ($pin, $pinName) = ParsePin($hash, $arg);
|
|
return "illegal pin $arg" if (!defined($pin)); # parsePin logs error if wrong pin spec
|
|
|
|
if ($aVal eq "0") {
|
|
UserReadingsDelete($hash, "lastMsg", $pin);
|
|
UserReadingsDelete($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);
|
|
}
|
|
}
|
|
ManageUserAttr($hash, $aName);
|
|
}
|
|
elsif ($cmd eq "del") {
|
|
if ($aName =~ 'pin(.*)') {
|
|
my $arg = $1;
|
|
if (!$hash->{Initialized}) { # no hello received yet
|
|
return; # accept value for now.
|
|
}
|
|
my ($pin, $pinName) = ParsePin($hash, $arg);
|
|
if (defined($pin)) {
|
|
DoWrite( $hash, "${pin}d");
|
|
}
|
|
|
|
}
|
|
elsif ($aName eq 'disable') {
|
|
Log3 $name, 3, "$name: disable attribute removed";
|
|
DoOpen($hash) if ($init_done); # if fhem is initialized
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
#########################################################################
|
|
# flash a device via serial or OTA with external commands
|
|
sub DoFlash {
|
|
my ($hash, @args) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $log = "";
|
|
my @deviceName = split(/@/, $hash->{DeviceName});
|
|
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 if ($hash->{DeviceName} eq 'none');
|
|
my $hexFile = shift @args;
|
|
my $netPort = 0;
|
|
|
|
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]';
|
|
# https://github.com/esp8266/Arduino/blob/master/tools/espota.py
|
|
}
|
|
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 device at $port with $hexFile. See $logFile for details";
|
|
|
|
$log .= "flashing device as ArduCounter for $name\n";
|
|
$log .= "firmware file: $hexFile\n";
|
|
|
|
$log .= "port: $port\n";
|
|
$log .= "log file: $logFile\n";
|
|
|
|
if($flashCommand) {
|
|
if (-e $logFile) {
|
|
unlink $logFile;
|
|
}
|
|
DevIo_Disconnected($hash); # close, add to readyFnList so _Ready is called to reopen, set state
|
|
|
|
$log .= "$name closed\n";
|
|
|
|
$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: $flashCommand\n\n";
|
|
`$flashCommand`;
|
|
|
|
local $/=undef;
|
|
if (-e $logFile) {
|
|
open my $FILE, '<', $logFile;
|
|
my $logText = <$FILE>;
|
|
close $FILE;
|
|
$log .= "--- flash command ---------------------------------------------------------------------------------\n";
|
|
$log .= $logText;
|
|
$log .= "--- flash command ---------------------------------------------------------------------------------\n\n";
|
|
}
|
|
else {
|
|
$log .= "WARNING: flash command created no log file\n\n";
|
|
}
|
|
delete $hash->{Initialized};
|
|
my $now = gettimeofday();
|
|
my $delay = 5; # wait 5 seconds to give device time for reboot
|
|
Log3 $name, 4, "$name: DoFlash 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;
|
|
}
|
|
|
|
|
|
|
|
#####################################################
|
|
# parse pin input and return pin number and pin name
|
|
sub ParsePin {
|
|
my ($hash, $arg) = @_;
|
|
my $name = $hash->{NAME};
|
|
if ($arg !~ /^([DA]?)(\d+)/) {
|
|
Log3 $name, 3, "$name: parseTime got invalid pin spec $arg";
|
|
return;
|
|
}
|
|
my $pinType = $1;
|
|
my $pin = $2;
|
|
$pin = InternalPinNumber($hash, $pinType.$pin) if ($pinType eq 'A');
|
|
my $pinName = 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;
|
|
}
|
|
}
|
|
return ($pin, $pinName);
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
# clears all counter readings for a specified pin number
|
|
# called from set with a pin number
|
|
sub ClearPinCounters {
|
|
my ($hash, $pin) = @_;
|
|
DoWrite($hash, "${pin}c");
|
|
my ($err, $msg) = ReadAnswer($hash, '(cleared \d+)|(Error:)');
|
|
|
|
UserReadingsDelete($hash, 'pin', $pin); # internal device counter pinX
|
|
UserReadingsDelete($hash, 'long', $pin); # long counter longX
|
|
UserReadingsDelete($hash, 'interpolated', $pin); # interpolated long counter interpolatedLongX
|
|
UserReadingsDelete($hash, 'calcCounter', $pin); # calculated counter calcCounterX
|
|
UserReadingsDelete($hash, 'calcCounter_i', $pin); # calculated counter - ignored units calcCounterX_i
|
|
UserReadingsDelete($hash, 'power', $pin); # power reading powerX
|
|
UserReadingsDelete($hash, 'seq', $pin); # sequence seqX
|
|
|
|
UserReadingsDelete($hash, 'reject', $pin); # rejected pulsesin last reportng period rejectX
|
|
UserReadingsDelete($hash, 'timeDiff', $pin); # time difference of last reporting period timeDiffX
|
|
UserReadingsDelete($hash, 'countDiff', $pin); # count difference of last reporting period countDiffX
|
|
UserReadingsDelete($hash, 'lastMsg', $pin); # last message from device
|
|
|
|
UserReadingsDelete($hash, "runTime", $pin);
|
|
UserReadingsDelete($hash, "runTimeIgnore", $pin);
|
|
UserReadingsDelete($hash, ".switchOnTime", $pin);
|
|
UserReadingsDelete($hash, ".lastCheckIgnoreTime", $pin);
|
|
return;
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
# SET command
|
|
sub SetFn {
|
|
|
|
my @setValArr = @_; # remainder is set values
|
|
my $hash = shift @setValArr; # reference to Fhem device hash
|
|
my $name = shift @setValArr; # Fhem device name
|
|
my $setName = shift @setValArr; # name of the set option
|
|
my $setVal = join(' ', @setValArr); # set values as one string
|
|
|
|
return "\"set $name\" needs at least an argument" if (!$setName);
|
|
|
|
if(!defined($SetHash{$setName})) {
|
|
my @cList = keys %SetHash;
|
|
return "Unknown argument $setName, choose one of " . join(" ", @cList);
|
|
}
|
|
if ($setName eq "disable") {
|
|
Log3 $name, 4, "$name: set disable called";
|
|
CommandAttr(undef, "$name disable 1");
|
|
return;
|
|
}
|
|
elsif ($setName eq "enable") {
|
|
Log3 $name, 4, "$name: set enable called";
|
|
CommandAttr(undef, "$name disable 0");
|
|
return;
|
|
}
|
|
elsif ($setName eq "reconnect") {
|
|
Log3 $name, 4, "$name: set reconnect called";
|
|
if ($hash->{DeviceName} eq 'none') {
|
|
Log3 $name, 4, "$name: Simulate closing / reopening connection to none";
|
|
}
|
|
else {
|
|
DevIo_CloseDev($hash);
|
|
DevIo_setStates($hash, 'disconnected'); # no triggers
|
|
delete $hash->{OpenRetries};
|
|
DoOpen($hash);
|
|
}
|
|
return;
|
|
}
|
|
elsif ($setName eq "clearLevels") {
|
|
delete $hash->{analogLevels};
|
|
return;
|
|
}
|
|
elsif ($setName eq "clearHistory") {
|
|
# remove history
|
|
delete $hash->{History};
|
|
delete $hash->{HistoryPin};
|
|
delete $hash->{LastHistSeq};
|
|
delete $hash->{HistIdx};
|
|
return;
|
|
}
|
|
elsif ($setName eq "clearCounters") { # clear counters for a specific pin
|
|
my ($pin, $pinName) = ParsePin($hash, $setVal);
|
|
return "illegal pin $setVal" if (!defined($pin)); # parsePin logs error if wrong pin spec
|
|
|
|
Log3 $name, 4, "$name: Set $setName $setVal called - removing all readings for pin $pinName, internal $pin";
|
|
ClearPinCounters($hash, $pin);
|
|
}
|
|
elsif ($setName eq "counter") { # set counters for a specific pin
|
|
if ($setVal =~ /([AD]?\d+)[\s\,]+([\d\.]+)/) {
|
|
my $val = $2;
|
|
my ($pin, $pinName) = ParsePin($hash, $1);
|
|
return "illegal pin $setVal" if (!defined($pin)); # parsePin logs error if wrong pin spec
|
|
Log3 $name, 4, "$name: Set $setName $setVal called - setting calcCounter for pin $pinName";
|
|
readingsBeginUpdate($hash);
|
|
UserBulkUpdate($hash, 'calcCounter', $pin, $val);
|
|
readingsEndUpdate($hash,1);
|
|
} else {
|
|
return "wrong syntax, use set counters pin value";
|
|
}
|
|
}
|
|
elsif ($setName eq "flash") {
|
|
return DoFlash($hash, @setValArr);
|
|
}
|
|
|
|
if(!IsOpen($hash)) {
|
|
Log3 $name, 4, "$name: Set $setName $setVal called but device is disconnected";
|
|
return "Set called but device is disconnected";
|
|
}
|
|
if (IsDisabled($name)) {
|
|
Log3 $name, 4, "$name: set $setName $setVal called but device is disabled";
|
|
return;
|
|
}
|
|
|
|
if ($setName eq "raw") {
|
|
Log3 $name, 4, "$name: set raw $setVal called";
|
|
DoWrite($hash, "$setVal");
|
|
}
|
|
elsif ($setName eq "saveConfig") {
|
|
Log3 $name, 4, "$name: set saveConfig called";
|
|
DoWrite($hash, "e");
|
|
}
|
|
elsif ($setName eq "reset") {
|
|
Log3 $name, 4, "$name: set reset called";
|
|
if (DoWrite($hash, "r")) {
|
|
delete $hash->{Initialized};
|
|
}
|
|
if ($hash->{DeviceName} eq 'none') {
|
|
Log3 $name, 4, "$name: Simulate closing and reopening connection to none";
|
|
}
|
|
else {
|
|
DevIo_CloseDev($hash);
|
|
DevIo_setStates($hash, 'disconnected'); # no triggers
|
|
DoOpen($hash);
|
|
}
|
|
return "sent (r)eset command to device - waiting for its setup message";
|
|
}
|
|
elsif ($setName eq "resetWifi") {
|
|
Log3 $name, 4, "$name: set resetWifi called";
|
|
DoWrite($hash, "w");
|
|
return "sent (w) command to device";
|
|
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
# GET command
|
|
sub GetFn {
|
|
my @getValArr = @_; # rest is optional values
|
|
my $hash = shift @getValArr; # reference to device hash
|
|
my $name = shift @getValArr; # device name
|
|
my $getName = shift @getValArr; # get option name
|
|
my $getVal = join(' ', @getValArr); # optional value after get name
|
|
|
|
return "\"get $name\" needs at least one argument" if (!$getName);
|
|
|
|
if(!defined($GetHash{$getName})) {
|
|
my @cList = keys %GetHash;
|
|
return "Unknown argument $getName, choose one of " . join(" ", @cList);
|
|
}
|
|
if ($getName eq "levels") {
|
|
my $msg = "";
|
|
foreach my $level (sort {$a <=> $b} keys %{$hash->{analogLevels}}) {
|
|
$msg .= "$level: $hash->{analogLevels}{$level}\n";
|
|
}
|
|
return "observed levels from analog input:\n$msg\n";
|
|
}
|
|
if(!IsOpen($hash)) {
|
|
Log3 $name, 4, "$name: Get called but device is disconnected";
|
|
return ("Get called but device is disconnected", undef);
|
|
}
|
|
if (IsDisabled($name)) {
|
|
Log3 $name, 4, "$name: get called but device is disabled";
|
|
return;
|
|
}
|
|
|
|
if ($getName eq "info") {
|
|
Log3 $name, 5, "$name: Sending info command to device";
|
|
DoWrite( $hash, "s");
|
|
my ($err, $msg) = ReadAnswer($hash, 'Next report in.*seconds');
|
|
return ($err ? $err : $msg);
|
|
}
|
|
elsif ($getName eq "history") {
|
|
Log3 $name, 5, "$name: get history";
|
|
$hash->{HistIdx} = 0 if (!defined($hash->{HistIdx}));
|
|
my $idx = $hash->{HistIdx}; # HistIdx points to the next slot to be overwritten
|
|
my $ret = "";
|
|
my $count = 0;
|
|
my $histLine;
|
|
while ($count < AttrVal($name, "maxHist", 1000)) {
|
|
if (defined ($hash->{History}[$idx])) {
|
|
if (!$getVal || !$hash->{HistoryPin} || $hash->{HistoryPin}[$idx] eq $getVal) {
|
|
$ret .= $hash->{History}[$idx] . "\n";
|
|
}
|
|
}
|
|
$idx++;
|
|
$count++;
|
|
$idx = 0 if ($idx > AttrVal($name, "maxHist", 1000));
|
|
}
|
|
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;
|
|
}
|
|
|
|
|
|
###########################################
|
|
# 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 ParseTime {
|
|
my ($hash, $line, $now) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
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'}";
|
|
}
|
|
}
|
|
else {
|
|
$hash->{'.DeTOff'} = $now - $deviceNowSecs;
|
|
$hash->{'.Drift2'} = 0;
|
|
$hash->{'.DriftStart'} = $now;
|
|
Log3 $name, 5, "$name: Initialize device clock offset to $hash->{'.DeTOff'}";
|
|
}
|
|
$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) . "%" : "");
|
|
return;
|
|
}
|
|
|
|
|
|
sub ParseAvailablePins {
|
|
my ($hash, $line) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
Log3 $name, 5, "$name: Device sent available pins $line";
|
|
# 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;
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
####################################################
|
|
# Hello is sent after reconnect or restart
|
|
# check firmware version, set device boot time hash
|
|
# set timer to configure device
|
|
sub ParseHello {
|
|
my ($hash, $line, $now) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
# current versions send Time and avaliable pins as separate lines, not here
|
|
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, 5, "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, 5, "$name: device sent hello: $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);
|
|
|
|
ParseTime($hash, $dTime, $now) if ($dTime);
|
|
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
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
#########################################################################################
|
|
# 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 UserReadingName {
|
|
my ($hash, $rBaseName, $pin) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $pinName = PinName ($hash, $pin);
|
|
my $verbose = AttrValFromList($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 AttrValFromList($hash, $default, "readingNameCount$pinName", "readingNameCount$pin");
|
|
}
|
|
elsif ($rBaseName eq 'long') {
|
|
my $default = ($verbose >= 0 ? "long$pinName" : ".long$pinName"); # hidden if verboseReadings < 0
|
|
return AttrValFromList($hash, $default, "readingNameLongCount$pinName", "readingNameLongCount$pin");
|
|
}
|
|
elsif ($rBaseName eq 'interpolated') {
|
|
my $default = ($verbose >= 0 ? "interpolatedLong$pinName" : ""); # no reading if verboseReadings < 0
|
|
return AttrValFromList($hash, $default, "readingNameInterpolatedCount$pinName", "readingNameInterpolatedCount$pin");
|
|
}
|
|
elsif ($rBaseName eq 'calcCounter') {
|
|
return AttrValFromList($hash, "calcCounter$pinName", "readingNameCalcCount$pinName", "readingNameCalcCount$pin");
|
|
}
|
|
elsif ($rBaseName eq 'calcCounter_i') {
|
|
return AttrValFromList($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 AttrValFromList($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;
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
# return the value of the reading
|
|
# with a passed internal name and a pin number
|
|
# depending on verboseReadings and readingName attributes
|
|
sub UserReadingsVal {
|
|
my ($name, $rBaseName, $pin, $default) = @_;
|
|
my $hash = $defs{$name};
|
|
|
|
$default = 0 if (!defined($default));
|
|
my $rName = UserReadingName($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 UserReadingsTimestamp {
|
|
my ($name, $rBaseName, $pin) = @_;
|
|
my $hash = $defs{$name};
|
|
|
|
my $rName = UserReadingName($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 UserBulkUpdate {
|
|
my ($hash, $rBaseName, $pin, $value, $sTime) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $rName = UserReadingName($hash, $rBaseName, $pin);
|
|
if (!$rName) {
|
|
#Log3 $name, 5, "UserBulkUpdate - 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 UserReadingsDelete {
|
|
my ($hash, $rBaseName, $pin) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $rName = UserReadingName($hash, $rBaseName, $pin);
|
|
readingsDelete($hash, $rName);
|
|
return;
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
sub HandleCounters {
|
|
my ($hash, $pin, $seq, $count, $time, $diff, $rDiff, $now, $ppu) = @_;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $pinName = PinName ($hash, $pin);
|
|
my $lName = LogPinDesc($hash, $pin);
|
|
my $pLog = "$name: pin $pinName ($lName)"; # to be used as start of log lines
|
|
|
|
my $longCount = UserReadingsVal($name, 'long', $pin); # alter long count Wert
|
|
my $intpCount = UserReadingsVal($name, 'interpolated', $pin); # alter interpolated count Wert
|
|
my $lastCount = UserReadingsVal($name, 'pin', $pin);
|
|
my $cCounter = UserReadingsVal($name, 'calcCounter', $pin); # calculated counter
|
|
my $iSum = UserReadingsVal($name, 'calcCounter_i', $pin); # ignored sum
|
|
my $lastSeq = UserReadingsVal($name, 'seq', $pin);
|
|
my $intrCount = 0; # interpolated count to be added
|
|
|
|
my $lastCountTS = UserReadingsTimestamp ($name, 'pin', $pin); # last time long count reading was set as string
|
|
my $lastCountTNum = time_str2num($lastCountTS); # time as number
|
|
|
|
my $fBootTim;
|
|
my $deviceBooted;
|
|
if ($hash->{deviceBooted} && $lastCountTS && $hash->{deviceBooted} > $lastCountTNum) {
|
|
$deviceBooted = 1; # first report for this pin after a restart
|
|
$fBootTim = FmtTime($hash->{deviceBooted}) ; # time device booted
|
|
} # without old readings, interpolation makes no sense anyway
|
|
|
|
my $countStart = $count - $rDiff; # count at start of this reported interval
|
|
$countStart = 0 if ($countStart < 0);
|
|
|
|
my $timeGap = ($now - $time/1000 - $lastCountTNum); # time between last report and start of currently reported interval
|
|
$timeGap = 0 if ($timeGap < 0 || !$lastCountTS);
|
|
|
|
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);
|
|
}
|
|
|
|
my $pulseGap = $countStart - $lastCount; # gap of missed pulses if any
|
|
$pulseGap = 0 if (!$lastCountTS); # readings didn't exist yet
|
|
|
|
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 " . 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 = UserReadingsVal ($name, "timeDiff", $pin); # time diff of last interval (old reading)
|
|
my $lastCDiff = UserReadingsVal ($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
|
|
my $lastRatio = $lastCDiff / $lastInterval;
|
|
my $curRatio = $diff / $time;
|
|
my $intRatio = 1000 * ($lastRatio + $curRatio) / 2;
|
|
$intrCount = int(($offlTime * $intRatio)+0.5);
|
|
Log3 $name, 3, "$pLog interpolating for $offlTime secs until boot, $intrCount estimated pulses (before $lastCDiff in $lastInterval ms, now $diff in $time ms, avg ratio $intRatio p/s)";
|
|
}
|
|
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" if ($rDiff);
|
|
Log3 $name, 5, "$pLog adding interpolated $intrCount to interpolated count $intpCount" if ($intrCount);
|
|
|
|
$intpCount += ($rDiff + $pulseGap + $intrCount);
|
|
$longCount += ($rDiff + $pulseGap);
|
|
if ($ppu) {
|
|
$cCounter += ($rDiff + $pulseGap + $intrCount) / $ppu; # add to calculated counter
|
|
$iSum += $intrCount / $ppu; # sum of interpolation kWh
|
|
}
|
|
|
|
UserBulkUpdate($hash, 'pin', $pin, $count);
|
|
UserBulkUpdate($hash, 'long', $pin, $longCount);
|
|
UserBulkUpdate($hash, 'interpolated', $pin, $intpCount);
|
|
UserBulkUpdate($hash, 'calcCounter', $pin, $cCounter) if ($ppu);
|
|
UserBulkUpdate($hash, 'calcCounter_i', $pin, $iSum) if ($ppu);
|
|
UserBulkUpdate($hash, 'seq', $pin, $seq);
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
|
|
#########################################################################
|
|
sub 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) {
|
|
readingsDelete($hash, "runTime$pinName");
|
|
readingsDelete($hash, "runTimeIgnore$pinName");
|
|
readingsDelete($hash, ".switchOnTime$pinName");
|
|
readingsDelete($hash, ".lastCheckIgnoreTime$pinName");
|
|
return;
|
|
}
|
|
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";
|
|
}
|
|
|
|
# check if an ignore device is on so runtime is not added currently
|
|
my $doIgnore = 0;
|
|
my $ignoreSpec = AttrValFromList($hash, "", "runTimeIgnore$pinName", "runTimeIgnore$pin");
|
|
my @devices = devspec2array($ignoreSpec);
|
|
#Log3 $name, 5, "$name: HandleRunTime: devices list is @devices";
|
|
DEVICELOOP:
|
|
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/) {
|
|
$doIgnore = 1;
|
|
Log3 $name, 5, "$name: HandleRunTime: ignoreDevice $d is $state";
|
|
last DEVICELOOP;
|
|
}
|
|
}
|
|
|
|
my $iTime = ReadingsVal($name, "runTimeIgnore$pinName", 0); # time to ignore accumulated
|
|
if ($doIgnore) { # ignore device is on
|
|
my $siTime = ReadingsVal($name, ".lastCheckIgnoreTime$pinName",0); # last time we saw ignore device on
|
|
if ($siTime) {
|
|
my $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
|
|
}
|
|
#Log3 $name, 5, "$name: HandleRunTime: setze .lastCheckIgnoreTime auf now";
|
|
readingsBulkUpdate($hash, ".lastCheckIgnoreTime$pinName", $now); # last time we saw ignore device on
|
|
}
|
|
else {
|
|
Log3 $name, 5, "$name: HandleRunTime: no ignoreDevice is on, lösche .lastCheckIgnoreTime";
|
|
readingsDelete($hash, ".lastCheckIgnoreTime$pinName"); # no ignore device is on -> remove marker for last time on
|
|
}
|
|
|
|
my $rTime = int($now - $soTime); # time since water was switched on
|
|
my $newRunTime = $rTime - $iTime; # time since switch on minus ignore time
|
|
Log3 $name, 5, "$name: HandleRunTime: runTime is now: $rTime - $iTime = $newRunTime";
|
|
readingsBulkUpdate($hash, "runTime$pinName", $newRunTime); # set new runtime reading
|
|
return;
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
sub 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+))?/)
|
|
{
|
|
# new count is beeing reported
|
|
my ($pin, $count, $diff, $rDiff, $time, $dTime, $reject, $seq, $avgLen) =
|
|
($1, $2, $3, $4, $5, $6, $7, $8, $9);
|
|
my $pinName = PinName($hash, $pin);
|
|
my $power;
|
|
|
|
# first try with pinName, then pin Number, then generic fallback for all pins
|
|
my $factor = AttrValFromList($hash, 1000, "readingFactor$pinName", "readingFactor$pin", "factor");
|
|
my $ppu = AttrValFromList($hash, 0, "readingPulsesPerKWh$pinName", "readingPulsesPerKWh$pin", "pulsesPerKWh");
|
|
$ppu = AttrValFromList($hash, $ppu, "readingPulsesPerUnit$pin", "readingPulsesPerUnit$pinName", "pulsesPerUnit");
|
|
my $fut = AttrValFromList($hash, 3600, "readingFlowUnitTime$pin", "readingFlowUnitTime$pinName", "flowUnitTime");
|
|
my $doRTime = AttrValFromList($hash, 0, "runTime$pinName", "runTime$pin");
|
|
my $doSTime = AttrValFromList($hash, 0, "readingStartTime$pinName", "readingStartTime$pin");
|
|
my $lName = 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
|
|
|
|
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;
|
|
}
|
|
|
|
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" : "") . (defined($ppu) ? ", PPU ${ppu}" : "") .
|
|
(defined($fut) ? ", FUT ${fut}s" : "") . ", result $powerFmt";
|
|
|
|
my $lastPower = UserReadingsVal($name, 'power', $pin); # alter Power Wert
|
|
readingsBeginUpdate($hash);
|
|
UserBulkUpdate($hash, 'power', $pin, $powerFmt, ($doSTime ? $sTime : undef));
|
|
|
|
#Log3 $name, 5, "$pLog last power $lastPower, power $power";
|
|
HandleRunTime($hash, $pinName, $pin, $lastPower, $powerFmt) if ($doRTime);
|
|
|
|
if (defined($reject) && $reject ne "") {
|
|
my $rejCount = ReadingsVal($name, "reject$pinName", 0); # alter reject count Wert
|
|
UserBulkUpdate($hash, 'reject', $pin, $reject + $rejCount);
|
|
}
|
|
UserBulkUpdate($hash, 'timeDiff', $pin, $time); # used internally for interpolation
|
|
UserBulkUpdate($hash, 'countDiff', $pin, $diff); # used internally for interpolation
|
|
UserBulkUpdate($hash, 'lastMsg', $pin, $line);
|
|
|
|
HandleCounters($hash, $pin, $seq, $count, $time, $diff, $rDiff, $now, $ppu);
|
|
readingsEndUpdate($hash, 1);
|
|
|
|
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
|
|
AskForHello("direct:$name");
|
|
}
|
|
RemoveInternalTimer ("sendHello:$name"); # don't send hello again
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
sub HandleHistory {
|
|
my ($hash, $now, $pinName, $hist) = @_;
|
|
my $name = $hash->{NAME};
|
|
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])(?:\/(\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 = "pulse counted"}
|
|
elsif ($act eq "G") {$action = "gap"}
|
|
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)) { # probably wrap
|
|
$hash->{History}[$hash->{HistIdx}] = $histLine;
|
|
$hash->{HistoryPin}[$hash->{HistIdx}] = $pinName;
|
|
$hash->{LastHistSeq} = $seq;
|
|
$hash->{HistIdx}++;
|
|
}
|
|
$hash->{HistIdx} = 0 if ($hash->{HistIdx} > AttrVal($name, "maxHist", 1000));
|
|
}
|
|
else {
|
|
Log3 $name, 5, "$name: HandleHistory - no match for $he";
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
#########################################################################
|
|
sub Parse {
|
|
my ($hash) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $retStr = "";
|
|
|
|
my @lines = split /\n/, $hash->{buffer};
|
|
my $now = gettimeofday();
|
|
|
|
foreach my $line (@lines) {
|
|
$line =~ s/[\x0A\x0D]//g;
|
|
#Log3 $name, 5, "$name: Parse line: #" . $line . "#";
|
|
|
|
if ($line =~ /^ArduCounter V([\d\.]+).*(Started|Hello)/) { # setup / hello message
|
|
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");
|
|
delete $hash->{KeepAliveRetries};
|
|
readingsSingleUpdate($hash, "RSSI", $rssi, 1) if ($rssi);
|
|
}
|
|
elsif ($line =~ /^R([\d]+)(.*)/) { # report counters
|
|
ParseReport($hash, $line);
|
|
$retStr .= ($retStr ? "\n" : "") . "report for pin $1: $2";
|
|
}
|
|
elsif ($line =~ /^C([0-9\,]+)/) { # available pins
|
|
ParseAvailablePins($hash, $1);
|
|
$retStr .= ($retStr ? "\n" : "") . "available pins: $1";
|
|
}
|
|
elsif ($line =~ /^H([\d]+) (.+)/) { # pin pulse history as separate line
|
|
my $pin = $1;
|
|
my $hist = $2;
|
|
my $pinName = PinName($hash, $pin);
|
|
HandleHistory($hash, $now, $pinName, $hist);
|
|
if (AttrValFromList($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 =~ /^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
|
|
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";
|
|
SetDisconnected($hash); # set to disconnected (state), remove timers
|
|
if ($hash->{DeviceName} eq 'none') {
|
|
Log3 $name, 4, "$name: Simulate closing connection to none";
|
|
}
|
|
else {
|
|
DevIo_CloseDev($hash); # close, remove from readyfnlist so _ready is not called again
|
|
DevIo_setStates($hash, 'disconnected'); # no triggers
|
|
}
|
|
|
|
RemoveInternalTimer ("delayedopen:$name");
|
|
InternalTimer($now+$delay, "ArduCounter::DelayedOpen", "delayedopen:$name", 0);
|
|
# 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+: *([\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" : "") . $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 {
|
|
Log3 $name, 3, "$name: unparseable message from device: $line";
|
|
}
|
|
}
|
|
$hash->{buffer} = "";
|
|
return $retStr;
|
|
}
|
|
|
|
|
|
|
|
#########################################################################
|
|
# called from the global loop, when the select for hash->{FD} reports data
|
|
sub ReadFn {
|
|
my $hash = shift;
|
|
my $name = $hash->{NAME};
|
|
my ($pin, $count, $diff, $power, $time, $reject, $msg);
|
|
my $buf;
|
|
|
|
if ($hash->{DeviceName} eq 'none') { # simulate receiving
|
|
if ($hash->{TestInput}) {
|
|
$buf = $hash->{TestInput};
|
|
delete $hash->{TestInput};
|
|
}
|
|
}
|
|
else {
|
|
# read from serial device
|
|
$buf = DevIo_SimpleRead($hash);
|
|
return if (!defined($buf) );
|
|
}
|
|
|
|
$hash->{buffer} .= $buf;
|
|
my $end = chop $buf;
|
|
#Log3 $name, 5, "$name: Read: current buffer content: " . $hash->{buffer};
|
|
|
|
# did we already get a full frame?
|
|
return if ($end ne "\n");
|
|
Parse($hash);
|
|
return;
|
|
}
|
|
|
|
|
|
#####################################
|
|
# Called from get / set to get a direct answer
|
|
# called with logical device hash
|
|
sub ReadAnswer {
|
|
my ($hash, $expect) = @_;
|
|
my $name = $hash->{NAME};
|
|
my $rin = '';
|
|
my $msgBuf = '';
|
|
my $to = AttrVal($name, "timeout", 2);
|
|
my $buf;
|
|
|
|
Log3 $name, 5, "$name: ReadAnswer called";
|
|
|
|
for(;;) {
|
|
if ($hash->{DeviceName} eq 'none') { # simulate receiving
|
|
$buf = $hash->{TestInput};
|
|
delete $hash->{TestInput};
|
|
}
|
|
elsif($^O =~ m/Win/ && $hash->{USBDev}) {
|
|
$hash->{USBDev}->read_const_time($to*1000); # set timeout (ms)
|
|
$buf = $hash->{USBDev}->read(999);
|
|
if(length($buf) == 0) {
|
|
Log3 $name, 3, "$name: Timeout in ReadAnswer";
|
|
return ("Timeout reading answer", undef)
|
|
}
|
|
}
|
|
else {
|
|
if(!$hash->{FD}) {
|
|
Log3 $name, 3, "$name: Device lost in ReadAnswer";
|
|
return ("Device lost when reading answer", undef);
|
|
}
|
|
|
|
vec($rin, $hash->{FD}, 1) = 1; # setze entsprechendes Bit in rin
|
|
my $nfound = select($rin, undef, undef, $to);
|
|
if($nfound < 0) {
|
|
next if ($! == EAGAIN() || $! == EINTR() || $! == 0);
|
|
my $err = $!;
|
|
SetDisconnected($hash); # set to disconnected, remove timers, let _ready try to reopen
|
|
Log3 $name, 3, "$name: ReadAnswer error: $err";
|
|
return("ReadAnswer error: $err", undef);
|
|
}
|
|
if($nfound == 0) {
|
|
Log3 $name, 3, "$name: Timeout2 in ReadAnswer";
|
|
return ("Timeout reading answer", undef);
|
|
}
|
|
|
|
$buf = DevIo_SimpleRead($hash);
|
|
if(!defined($buf)) {
|
|
Log3 $name, 3, "$name: ReadAnswer got no data";
|
|
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 .= Parse($hash);
|
|
|
|
#Log3 $name, 5, "$name: ReadAnswer msgBuf: " . $msgBuf;
|
|
if ($msgBuf =~ $expect) {
|
|
Log3 $name, 5, "$name: ReadAnswer matched $expect";
|
|
return (undef, $msgBuf);
|
|
}
|
|
}
|
|
return ("no Data", undef);
|
|
}
|
|
|
|
|
|
|
|
1;
|
|
|
|
=pod
|
|
=item device
|
|
=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
|
|
|
|
<a id="ArduCounter"></a>
|
|
<h3>ArduCounter</h3>
|
|
|
|
<ul>
|
|
This module implements an Interface to an Arduino, ESP8266 or ESP32 based counter for pulses on any input pin of an Arduino Uno, Nano, Jeenode,
|
|
NodeMCU, Wemos D1, TTGO T-Display or similar device. <br>
|
|
The device connects to Fhem either through USB / serial or via Wifi / TCP if an ESP board is used.<br>
|
|
ArduCounter does not only count pulses but also measure pulse lenghts and the time between pulses so it can filter noise / bounces
|
|
and gives better power/flow (Watts or liters/second) readings than systems that just count in fixed time intervals.<br>
|
|
The number of pulses per kWh or liter is defineable and counters continue even when Fhem or the device restarts
|
|
so you don't need additional user readings to make such calculations<br>
|
|
The typical use case is an S0-Interface on an energy meter or water meter, but also reflection light barriers
|
|
to monitor old ferraris counters or analog water meters are supported<br>
|
|
Counters are configured with attributes that define which GPIO pins should count pulses and in which intervals the board should report the current counts.<br>
|
|
The sketch that works with this module uses pin change interrupts so it can efficiently count pulses on all available input pins.
|
|
The module has been tested with 14 inputs of an Arduino Uno counting in parallel and pulses as short as 3 milliseconds.<br>
|
|
The module creates readings for pulse counts, consumption and optionally also a pin history with pulse lengths and gaps of the last pulses.<br>
|
|
If an ESP8266 or ESP32 is used, the device can be flashed and configured over Wifi (it opens its own temporary Hotspot / SSID for configuration
|
|
so you can set which existing SSID to connect to and which password to use). For TTGO T-Display boards (ESP32 with TFT display)
|
|
the local display on the device itself can also display Wifi status and current consumption.<br>
|
|
<br>
|
|
<b>Prerequisites</b>
|
|
<ul>
|
|
<br>
|
|
<li>
|
|
This module requires an Arduino Uno, Nano, Jeenode, NodeMCU, Wemos D1, TTGO T-Display or similar device based on an Atmel 328p, ESP8266 or ESP32
|
|
running the ArduCounter sketch provided with this module<br>
|
|
In order to flash an arduino board with the corresponding ArduCounter firmware from within Fhem, avrdude needs to be installed.<br>
|
|
To flash ESP32 or ESP8266 boards form within Fhem, Python and the scripts esptool.py / espota.py need to be installed.<br>
|
|
For old ferraris counters a reflection light barrier which in the simpest case can consist of a photo transistor (connected to an anlalog input of the Arduino / ESP)
|
|
and an led or a laser module (connected to a digital output), both with a resistor in line are needed.
|
|
To drive a laser module with 5V, another transistor is typically needed to switch 5V from a 3.3V GPIO output.
|
|
</li>
|
|
</ul>
|
|
<br>
|
|
|
|
<a id="ArduCounter-define"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<br>
|
|
<code>define <name> ArduCounter <device></code><br>
|
|
or<br>
|
|
<code>define <name> ArduCounter <ip:port></code><br>
|
|
<br>
|
|
<device> specifies the serial port to communicate with the Arduino.<br>
|
|
<ip:port> specifies the ip address and tcp port to communicate with an esp8266 / ESP32 where port is typically 80.<br>
|
|
|
|
The name of the serial device depends on your distribution and serial adapter.<br>
|
|
You can also specify a baudrate for serial connections if the device name contains the @ character, e.g.: /dev/ttyUSB0@115200<br>
|
|
The default baudrate of the ArduCounter firmware is 115200 since sketch version 4 and used to be 38400 since sketch version 1.4<br>
|
|
The latest version of this module will however try different baudrates automatically if communication with the counting device seems not possible.
|
|
<br>
|
|
Example:<br>
|
|
<br>
|
|
<ul><code>define AC ArduCounter /dev/ttyUSB2@115200</code></ul>
|
|
<ul><code>define AC ArduCounter 192.168.1.134:80</code></ul>
|
|
</ul>
|
|
<br>
|
|
|
|
<a id="ArduCounter-configuration"></a>
|
|
<b>Configuration of ArduCounter digital counters</b><br><br>
|
|
<ul>
|
|
Specify the pins where impulses should be counted e.g. as <code>attr AC pinX falling pullup min 25</code> <br>
|
|
The X in pinX can be an Arduino / ESP GPIO pin number with or without the letter D e.g. pin4, pinD5, pin6, pinD7 ...<br>
|
|
After the pin you can use the keywords falling or rising to define if a logical one / 5V (rising) or a logical zero / 0V (falling) should be treated as pulse.<br>
|
|
The optional keyword pullup activates the pullup resistor for the given Pin. <br>
|
|
The last argument is also optional but recommended and specifies a minimal pulse length in milliseconds.<br>
|
|
An energy meter with S0 interface is typically connected to GND and an input pin like D4. <br>
|
|
The S0 pulse then pulls the input down to 0V.<br>
|
|
Since the minimal pulse lenght of an S0 interface is specified to be 30ms, the typical configuration for an s0 interface is <br>
|
|
<code>attr AC pinX falling pullup min 25</code><br>
|
|
Specifying a minimal pulse length is recommended since it filters bouncing of reed contacts or other noise.
|
|
The keyword <code>min</code> before <code>25</code> is optional.
|
|
<br><br>
|
|
Example:<br>
|
|
<pre>
|
|
define AC ArduCounter /dev/ttyUSB2
|
|
attr AC pulsesPerUnit 1000
|
|
attr AC interval 60 300
|
|
attr AC pinD4 falling pullup min 5
|
|
attr AC pinD5 falling pullup min 25
|
|
attr AC pinD6 rising
|
|
</pre>
|
|
This defines a counter that is connected to Fhem via serial line ttyUSB2 with three counters connected to the GPIO pins D4, D5 and D5. <br>
|
|
D4 and D5 have their pullup resistors activated and the impulses draw the pins to zero.<br>
|
|
For D4 and D5 the board measures the time in milliseconds between the falling edge and the rising edge.
|
|
If this time is longer than the specified 5 (or 25 for pin D5) milliseconds then the impulse is counted. <br>
|
|
If the time is shorter then this impulse is regarded as noise and added to a separate reject counter.<br>
|
|
For pin D6 the board uses a default minimal length of 2ms and counts every time when the signal changes from 1 (rising pulse) back to 0.
|
|
</ul>
|
|
<br>
|
|
|
|
<b>Configuration of ArduCounter analog counters</b><br><br>
|
|
<ul>
|
|
This module and the corresponding ArduCounter sketch can be used to read water meters or old analog ferraris energy counters.<br>
|
|
Therefore a reflection light barrier needs to be connected to the board. This might simply consist of an infra red photo transistor
|
|
(connected to an analog input) and an infra red led (connected to a digital output), both with a resistor in line.
|
|
The idea comes from Martin Kompf (https://www.kompf.de/tech/emeir.html) and has been adopted for ArduCounter to support
|
|
old ferraris energy counters or water meters.<br>
|
|
The configuration is then similar to the one for digital counters:<br>
|
|
<pre>
|
|
define WaterMeter ArduCounter 192.168.1.110:80
|
|
attr ACF pinA0 rising pullup min 4 analog out 27 threshold 120,220
|
|
attr ACF interval 5,60,2,15,10,3
|
|
attr ACF pulsesPerUnit 35
|
|
attr ACF stateFormat {sprintf("%.3f l/min", ReadingsVal($name,"powerA0",0))}
|
|
</pre>
|
|
In this case an analog GPIO pin is used as input and the normal configuration parameters are followed by the keyword
|
|
<code>analog out</code> or simply <code>out</code>, the gpio number of a GPIO output that connects a light source and the thresholds
|
|
that decide when an analog input value is regarded as "low" or "high".
|
|
|
|
In the example an ESP32 is used via Wifi connection. GPIO pin A0 is used as analog input and is connected to a photo transistor that senses the intensity of light.
|
|
GPIO 27 is used as LED output and switched on/off in a high frequency. On GPIO A0 the reflected light is measured
|
|
and the difference in a measurement between when the LED is off and when the LED is on is compared to the thresholds defined in the pinA0-attribute.
|
|
When the measured light difference is above <code>220</code>, then a pulse starts (since <code>rising</code> is specified).
|
|
When the measured difference is below <code>120</code> then the pulse ends.<br>
|
|
<br>
|
|
The attribute <code>interval</code> has the following meaning in the above example:
|
|
The device reports the current counts and the time difference beween the first and the last pulse if at least 2 pulses have been counted
|
|
and if they are more than 15 milliseconds apart form each other. If not, then the device continues counting.
|
|
If after 60 seconds these conditions are stil not met, then the device will report the current count anyways and use the current time as the end of the interval.<br>
|
|
The last two numbers of the <code>interval</code> attribute define that the device will read the analog input 3 times and then work with the average.
|
|
Between each analog measurement series there will be a delay of 10 milliseconds.<br>
|
|
<br>
|
|
The attribute <code>pulsesPerUnit 35</code> defines that 35 pulses correspond to one unit (e.g. liter) and the reading <code>calcCounterA0</code>
|
|
is increased by the reported raw counts divided by 35.<br>
|
|
To find out the right analog thresholds you can set the attribute <code>enableHistory</code> to 1 which will ask the firmware of your counting board
|
|
to report the average difference measurements before they are compared to a threshold.
|
|
The ArduCounter module will count how often each value is reported and you can then query these analog level counts with <code>get levels</code>.
|
|
After a few measuremets the result of <code>get levels</code> might look like this:<br>
|
|
<pre>
|
|
observed levels from analog input:
|
|
94: 21
|
|
95: 79
|
|
96: 6
|
|
97: 2
|
|
98: 3
|
|
99: 2
|
|
100: 2
|
|
101: 1
|
|
102: 3
|
|
105: 2
|
|
106: 1
|
|
108: 2
|
|
109: 1
|
|
110: 1
|
|
112: 1
|
|
113: 3
|
|
115: 4
|
|
116: 9
|
|
117: 14
|
|
118: 71
|
|
119: 103
|
|
120: 118
|
|
121: 155
|
|
122: 159
|
|
123: 143
|
|
124: 147
|
|
125: 158
|
|
126: 198
|
|
127: 249
|
|
128: 220
|
|
129: 230
|
|
130: 201
|
|
131: 140
|
|
132: 147
|
|
133: 153
|
|
134: 141
|
|
135: 119
|
|
136: 105
|
|
137: 109
|
|
138: 114
|
|
139: 83
|
|
140: 33
|
|
141: 14
|
|
142: 1
|
|
</pre>
|
|
This shows the measured values together with the frequency how often the individual value has been measured.
|
|
It is obvious that most measurements result in values between 120 and 135, very few values are betweem 96 and 115
|
|
and another peak is around the value 95. <br>
|
|
It means that in the example of a ferraris energy counter, when the red mark of the ferraris disc is under the sensor,
|
|
the value is around 95 and while when the blank disc is under the sensor, the value is typically between 120 and 135.
|
|
So a good upper threshold would be 120 and a good lower threshold would be for example 96.
|
|
|
|
</ul>
|
|
<br>
|
|
|
|
<a id="ArduCounter-set"></a>
|
|
<b>Set-Commands</b><br>
|
|
<ul>
|
|
<li><b>raw</b></li>
|
|
send the value to the board so you can directly talk to the sketch using its commands.<br>
|
|
This is not needed for normal operation but might be useful sometimes for debugging
|
|
<li><b>flash [<file>]</b></li>
|
|
flashes the ArduCounter firmware from the subdirectory FHEM/firmware onto the device.<br>
|
|
Normally you can just specify <code>set myDevice flash</code>. 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 <code>board</code>) and depending on the connection (serial or Wifi).<br>
|
|
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 <br>
|
|
(by default it looks for ArduCounter.hex in the FHEM/firmware subdirectory).<br>
|
|
For an Arduino UNO for example the default is <code>avrdude -p atmega328P -c arduino -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]</code><br>
|
|
For an Arduino Nano based counter <code>-b 57600</code> is added.<br>
|
|
For an ESP32 connected via Wifi, the module would call espota.py which will upload the firmware over the air.<br>
|
|
If the attribute flashCommand is not specified for an ESP32 based board connected via serial line, then the module uses the command
|
|
<code>
|
|
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
|
|
</code> for example which flashes the whole ESP32 with all the partitions. <br>
|
|
For over the air flashing it would use
|
|
<code> espota.py -i[IP] -p [NETPORT] -f [BINFILE] 2>[LOGFILE]</code>.<br>
|
|
Of course esptool.py or espota.py as well as python would need to be installed on the system.
|
|
|
|
<li><b>resetWifi</b></li>
|
|
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.
|
|
<li><b>reset</b></li>
|
|
sends a command to the device which causes a hardware reset or reinitialize and reset of the internal counters of the board. <br>
|
|
The module then reopens the counting device and resends the attribute configuration / definition of the pins.
|
|
<li><b>saveConfig</b></li>
|
|
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.
|
|
<li><b>enable</b></li>
|
|
sets the attribute disable to 0
|
|
<li><b>disable</b></li>
|
|
sets the attribute disable to 1
|
|
<li><b>reconnect</b></li>
|
|
closes the tcp connection to an ESP based counter board that is conected via TCP/IP and reopen the connection
|
|
<li><b>clearLevels</b></li>
|
|
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.
|
|
<li><b>clearCounters <pin></b></li>
|
|
resets all the counter readings for the specified pin to 0
|
|
<li><b>counter <pin>, <value></b></li>
|
|
set the calcCounter reading for the specified pin to the given value
|
|
<li><b>clearHistory</b></li>
|
|
deletes all the cached pin history entries
|
|
</ul>
|
|
<br>
|
|
<a id="ArduCounter-get"></a>
|
|
<b>Get-Commands</b><br>
|
|
<ul>
|
|
<li><b>info</b></li>
|
|
send a command to the Arduino board to get current counts.<br>
|
|
This is not needed for normal operation but might be useful sometimes for debugging
|
|
<li><b>levels</b></li>
|
|
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.
|
|
<li><b>history <pin></b></li>
|
|
shows details regarding all the level changes that the counter device (Arduino or ESP) has detected and how they were used (counted or rejected)<br>
|
|
If get history is issued with a pin name (e.g. get history D5) then only the history entries concerning D5 will be shown.<br>
|
|
This information is sent from the device to Fhem if the attribute <code>enableHistory</code> is set to 1.<br>
|
|
The maximum number of lines that the Arducounter module stores in a ring buffer is defined by the attribute maxHist and defaults to 1000.
|
|
</ul>
|
|
<br>
|
|
<a id="ArduCounter-attr"></a>
|
|
<b>Attributes</b><br><br>
|
|
<ul>
|
|
<li><a href="#do_not_notify">do_not_notify</a></li>
|
|
<li><a href="#readingFnAttributes">readingFnAttributes</a></li>
|
|
<br>
|
|
<li><a id="ArduCounter-attr-pin" data-pattern="pin.*">pin[AD]?[0-9]+<rising|falling> [<pullup>] [min] <min length> [[analog] out <out pin> [threshold] <min, max>]</a><br>
|
|
Define a GPIO pin of the Arduino or ESP board as input. This attribute expects for digital inputs either
|
|
<code>rising</code> or <code>falling</code>, followed by an optional <code>pullup</code> and the optional keyword <code>min</code>
|
|
and an optional number as minimal length of pulses and gaps between pulses.<br>
|
|
The counter device will track rising and falling edges of each impulse and measure the length of a pulse in milliseconds.<br>
|
|
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.<br>
|
|
Example:<br>
|
|
<pre>
|
|
attr MyCounter pinD4 falling pullup 25
|
|
</pre>
|
|
For analog inputs with connected reflective light barries, you have to add <code>analog out</code>
|
|
and the GPIO pin number of the pin where the light source (LED or laser) is connected, the keyword <code>threshold</code>
|
|
followed by the lower and upper threshold separated by a komma.<br>
|
|
Example:<br>
|
|
<pre>
|
|
attr MyCounter pinA0 rising pullup min 3 analog out 27 threshold 120,220
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-interval">interval <normal> <max> [<min> <min count> [<analog interval> <analog samples>]]</a><br>
|
|
Defines the parameters that affect the way counting and reporting works.
|
|
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.
|
|
<br><br>
|
|
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.<br>
|
|
After the normal interval is elapsed the Arduino board reports the count and time for those pins where impulses were encountered.<br>
|
|
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.<br>
|
|
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. <br>
|
|
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.
|
|
<br><br>
|
|
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.<br>
|
|
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.<br>
|
|
By adjusting the other numbers of this attribute this can be avoided.<br>
|
|
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.<br>
|
|
This way the counter will report a higher number of pulses counted and a larger time difference back to fhem. <br>
|
|
Example:<br>
|
|
<pre>
|
|
attr myCounter interval 60 600 5 2
|
|
</pre><br>
|
|
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.
|
|
<br><br>
|
|
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.
|
|
</li>
|
|
<li><a id="ArduCounter-attr-board">board</a><br>
|
|
specify the type of the board used for ArduCounter like NANO, UNO, ESP32, ESP8266 or T-Display<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter board NANO
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-pulsesPerUnit" data-pattern="pulsesPer.*">pulsesPerUnit <number></a><br>
|
|
specify the number of pulses that the meter is giving out per unit that sould be displayed (e.g. per kWh energy consumed). <br>
|
|
For many S0 counters this is 1000, for old ferraris counters this is 75 (rounds per kWh).<br>
|
|
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.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter pulsesPerUnit 75
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingPulsesPerUnit" data-pattern="readingPulsesPer.*">readingPulsesPerUnit[AD]?[0-9]+ <number></a><br>
|
|
is the same as pulsesPerUnit but specified per GPIO pin individually in case you have multiple counters with different settings at the same time<br>
|
|
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.<br>
|
|
<br>
|
|
Example:<br>
|
|
<pre>
|
|
attr myCounter readingPulsesPerUnitA7 75<br>
|
|
attr myCounter readingPulsesPerUnitD4 1000
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingFlowUnitTime" data-pattern="readingFlowUnitTime.*">readingFlowUnitTime[AD]?[0-9]+ <time></a><br>
|
|
specified the time period in seconds which is used as the basis for calculating the current flow or power for the given pin.<br>
|
|
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.<br>
|
|
If you count kWh and you want to see the current power in kW, then specify 3600 (one hour).<br>
|
|
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.
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-flowUnitTime" data-pattern="flowUnitTime">flowUnitTime <time></a><br>
|
|
like readingFlowUnitTimeXX but applies to all pins that have no explicit readingFlowUnitTimeXX attribute.
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingNameCount" data-pattern="readingNameCount">readingNameCount[AD]?[0-9]+ <new name></a><br>
|
|
Change the name of the counter reading pinX to something more meaningful. <br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter readingNameCountD4 CounterHaus_internal
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingNameLongCount" data-pattern="readingNameLongCount">readingNameLongCount[AD]?[0-9]+ <new name></a><br>
|
|
Change the name of the long counter reading longX to something more meaningful.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter readingNameLongCountD4 CounterHaus_long
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingNameInterpolatedCount" data-pattern="readingNameInterpolatedCount">readingNameInterpolatedCount[AD]?[0-9]+ <new name></a><br>
|
|
Change the name of the interpolated long counter reading InterpolatedlongX to something more meaningful.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter readingNameInterpolatedCountD4 CounterHaus_interpolated
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingNameCalcCount" data-pattern="readingNameCalcCount">readingNameCalcCount[AD]?[0-9]+ <new name></a><br>
|
|
Change the name of the real unit counter reading CalcCounterX to something more meaningful.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter readingNameCalcCountD4 CounterHaus_kWh
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingNamePower" data-pattern="readingNamePower">readingNamePower[AD]?[0-9]+ <new name></a><br>
|
|
Change the name of the power reading powerX to something more meaningful.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter readingNamePowerD4 PowerHaus_kW
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingStartTime" data-pattern="readingStartTime">readingStartTime[AD]?[0-9]+ [0|1]</a><br>
|
|
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.
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-verboseReadings" data-pattern="verboseReadings">verboseReadings[AD]?[0-9]+ [0|1]</a><br>
|
|
create the additional readings lastMsg and pinHistory for each pin<br>
|
|
if verboseReafings is set to 1 for the specified pin.<br>
|
|
If set to -1 then the internal counter, the long counter and interpolated long counter readings will be hidden.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter verboseReadingsD4 1
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-enableHistory" data-pattern="enableHistory">enableHistory [0|1]</a><br>
|
|
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 <code>get history</code>
|
|
The optput of <code>get history</code> will look like this:
|
|
<pre>
|
|
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
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-enableSerialEcho" data-pattern="enableSerialEcho">enableSerialEcho [0|1]</a><br>
|
|
tells the counting device to show diagnostic data over the serial line when connected via TCP
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-enablePinDebug" data-pattern="enablePinDebug">enablePinDebug [0|1]</a><br>
|
|
tells the counting device to show every level change of the defined input pins over the serial line or via TCP
|
|
</li>
|
|
<li><a id="ArduCounter-attr-enableAnalogDebug" data-pattern="enableAnalogDebug">enableAnalogDebug [0|1]</a><br>
|
|
tells the counting device to show every analog measurement of the defined analog input pins over the serial line or via TCP
|
|
</li>
|
|
<li><a id="ArduCounter-attr-enableDevTime" data-pattern="enableDevTime">enableDevTime [0|1]</a><br>
|
|
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
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-maxHist" data-pattern="maxHist">maxHist <max entries></a><br>
|
|
specifies how many pin history lines hould be buffered for "get history".<br>
|
|
This attribute defaults to 1000.
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-analogThresholds" data-pattern="analogThresholds">analogThresholds</a><br>
|
|
this Attribute is outdated. Please specify the analog thresholds for reflective light barrier input with the attribute "pin..."
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-flashCommand" data-pattern="flashCommand">flashCommand <new shell command></a><br>
|
|
overrides the default command to flash the firmware via Wifi (OTA) or serial line. It is recommended to not define this attribute. <br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter flashCommand avrdude -p atmega328P -c arduino -b 57600 -P [PORT] -D -U flash:w:[HEXFILE] 2>[LOGFILE]
|
|
</pre>
|
|
<code>[PORT]</code> is automatically replaced with the serial port for this device as it is specified in the <code>define</code> command.<br>
|
|
<code>[HEXFILE]</code> or <code>[BINFILE]</code> are synonyms and are both automatically replaced with the firmware file appropriate for the device.
|
|
For ESP32 boards <code>[HEXFILE]</code> would be replaced by ArduCounter-8266.bin for example.<br>
|
|
<code>[LOGFILE]</code> is automatically replaced ArduCounterFlash.log in the fhem log subdirectory.<br>
|
|
<code>[NETPORT]</code> 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.<br>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-keepAliveDelay" data-pattern="keepAliveDelay">keepAliveDelay <delay></a><br>
|
|
defines an interval in which the module sends keepalive messages to a counter device that is conected via tcp.<br>
|
|
This attribute is ignored if the device is connected via serial port.<br>
|
|
If the device doesn't reply within a defined timeout then the module closes and tries to reopen the connection.<br>
|
|
The module tells the device when to expect the next keepalive message and the device will also close the tcp connection if it doesn't see a keepalive message within the delay multiplied by 3<br>
|
|
The delay defaults to 10 seconds.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter keepAliveDelay 30
|
|
</pre>
|
|
|
|
</li>
|
|
<li><a id="ArduCounter-attr-keepAliveTimeout" data-pattern="keepAliveTimeout">keepAliveTimeout <seconds></a><br>
|
|
defines the timeout when wainting for a keealive reply (see keepAliveDelay)
|
|
The timeout defaults to 2 seconds.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter keepAliveTimeout 3
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-keepAliveRetries" data-pattern="keepAliveRetries">keepAliveRetries <max number of retries></a><br>
|
|
defines how often sending a keepalive is retried before the connection is closed and reopened.<br>
|
|
It defaults to 2.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter keepAliveRetries 3
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-nextOpenDelay" data-pattern="nextOpenDelay">nextOpenDelay <delay></a><br>
|
|
defines the time in seconds that the module waits before retrying to open a disconnected tcp connection. <br>
|
|
This defaults to 60 seconds.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter nextOpenDelay 20
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-openTimeout" data-pattern="openTimeout">openTimeout <timeout></a><br>
|
|
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.<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter openTimeout 5
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-silentReconnect" data-pattern="silentReconnect">silentReconnect [0|1]</a><br>
|
|
if set to 1, then it will set the loglevel for "disconnected" and "reappeared" messages to 4 instead of 3<br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter silentReconnect 1
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-deviceDisplay">deviceDisplay <pin> <unit> <flowUnit></a><br>
|
|
controls the unit strings that a local display on the counting device will show. <br>
|
|
Example:
|
|
<pre>
|
|
attr myCounter deviceDisplay 36,l,l/m
|
|
attr myCounter deviceDisplay 36,kWh,kW
|
|
</pre>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-disable" data-pattern="disable">disable [0|1]</a><br>
|
|
if set to 1 then the module is disabled and closes the connection to a counter device.<br>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-factor">factor</a><br>
|
|
Define a multiplicator for calculating the power from the impulse count and the time between the first and the last impulse. <br>
|
|
This attribute is outdated and unintuitive so you should avoid it. <br>
|
|
Instead you should specify the attribute pulsesPerUnit or readingPulsesPerUnit[0-9]+ (where [0-9]+ stands for the pin number).
|
|
</li>
|
|
<li><a id="ArduCounter-attr-readingFactor" data-pattern="readingFactor.*">readingFactor[AD]?[0-9]+</a><br>
|
|
Override the factor attribute for this individual pin. <br>
|
|
Just like the attribute factor, this is a rather cumbersome way to specify the pulses per kWh. <br>
|
|
Instead it is advised to use the attribute pulsesPerUnit or readingPulsesPerUnit[0-9]+ (where [0-9]+ stands for the pin number).
|
|
</li>
|
|
<li><a id="ArduCounter-attr-runTime" data-pattern="runTime">runTime[AD]?[0-9]+</a><br>
|
|
if this attribute is set for a pin, then a new reading will be created which accumulates the run time for this pin while consumption is greater than 0.<br>
|
|
This allows e.g. to check if a water meter shows water consumption for a time longer than X without stop.
|
|
</li>
|
|
<li><a id="ArduCounter-attr-runTimeIgnore" data-pattern="runTimeIgnore">runTimeIgnore[AD]?[0-9]+</a><br>
|
|
this allows to ignore consumption for the run time attribute while a certain other device is switched on.
|
|
</li>
|
|
<li><a id="ArduCounter-attr-devVerbose">devVerbose</a><br>
|
|
this attribute is outdated and has been replaced with the attributes
|
|
<code>enableHistory, enableSerialEcho, enablePinDebug, enableAnalogDebug, enableDevTime</code>
|
|
</li>
|
|
<li><a id="ArduCounter-attr-configDelay">configDelay</a><br>
|
|
specify the time to wait for the board to report its configuration before Fhem sends the commands to reconfigure the board
|
|
</li>
|
|
<li><a id="ArduCounter-attr-helloSendDelay">helloSendDelay</a><br>
|
|
specify the time to wait for the board to report its type before Fhem sends the commands to ask for it
|
|
</li>
|
|
<li><a id="ArduCounter-attr-helloWaitTime">helloWaitTime</a><br>
|
|
specify the time to wait for the board to report its type when Fhem has asked for it before a timeout occurs
|
|
</li>
|
|
</ul>
|
|
<br>
|
|
<b>Readings / Events</b><br>
|
|
<ul>
|
|
The module creates at least the following readings and events for each defined pin:
|
|
<li><b>calcCounter.*</b></li>
|
|
This is recommended reading for counting units based on the pulses and the attribute pulsesPerUnit. It is similar to interpolated long count
|
|
which keeps on counting up after fhem restarts but this counter will take the pulses per Unit attribute into the calculation und thus does not
|
|
count pulses but real Units (kWh, liters or some other unit that is applicable)<br>
|
|
The name of this reading can be changed with the attribute readingNameCalcCount[AD]?[0-9]+ where [AD]?[0-9]+ stands for the pin description e.g. D4.<br>
|
|
Another reading with the same name but ending in _i (e.g. calcCounterD4_i) will show how many kWh (or other units) of the above value is interpolated.
|
|
|
|
<li><b>pin.*</b> e.g. pinD4</li>
|
|
the current internal count at this pin (internal to the Arduino / ESP device, starts at 0 when the device restarts). <br>
|
|
The name of this reading can be changed with the attribute readingNameCount[AD]?[0-9]+ where [AD]?[0-9]+ stands for the pin description e.g. D4
|
|
|
|
<li><b>long.*</b> e.g. longD5</li>
|
|
long count which keeps on counting up after fhem restarts whereas the pin.* count is only a temporary internal count that starts at 0 when the arduino board starts.<br>
|
|
The name of this reading can be changed with the attribute readingNameLongCount[AD]?[0-9]+ where [AD]?[0-9]+ stands for the pin description e.g. D4
|
|
|
|
<li><b>interpolatedLong.*</b></li>
|
|
like long.* but when the Arduino restarts the potentially missed pulses are interpolated based on the pulse rate before the restart and after the restart.<br>
|
|
The name of this reading can be changed with the attribute readingNameInterpolatedCount[AD]?[0-9]+ where [AD]?[0-9]+ stands for the pin description e.g. D4
|
|
|
|
<li><b>reject.*</b></li>
|
|
counts rejected pulses that are shorter than the specified minimal pulse length.
|
|
|
|
<li><b>power.*</b></li>
|
|
the current calculated power / flow at this pin.<br>
|
|
The name of this reading can be changed with the attribute readingNamePower[AD]?[0-9]+ where [AD]?[0-9]+ stands for the pin description e.g. D4.<br>
|
|
This reading depends on the attributes pulsesPerUnit as well as readingFlowUnitTime or flowUnitTime for calculation
|
|
|
|
<li><b>pinHistory.*</b></li>
|
|
shows detailed information of the last pulses. This is only available when a minimal pulse length is specified for this pin. Also the total number of impulses recorded here is limited to 20 for all pins together. The output looks like -36/7:0C, -29/7:1G, -22/8:0C, -14/7:1G, -7/7:0C, 0/7:1G<br>
|
|
The first number is the relative time in milliseconds when the input level changed, followed by the length in milliseconds, the level and the internal action.<br>
|
|
-36/7:0C for example means that 36 milliseconds before the reporting started, the input changed to 0V, stayed there for 7 milliseconds and this was counted.<br>
|
|
|
|
<li><b>countDiff.*</b></li>
|
|
delta of the current count to the last reported one. This is used together with timeDiff.* to calculate the power consumption.
|
|
|
|
<li><b>timeDiff.*</b></li>
|
|
time difference between the first pulse in the current observation interval and the last one. Used togehter with countDiff to calculate the power consumption.
|
|
|
|
<li><b>seq.*</b></li>
|
|
internal sequence number of the last report from the board to Fhem.
|
|
<li><b>runTime.*</b></li>
|
|
this reading will only be created when the attribute runTime[AD]?[0-9]+ is set for a given pin.<br>
|
|
It contains the time in seconds that the consumption / flow observed at the specified pin has not ben zero.<br>
|
|
If a water meter which outputs 10 impulses per liter on its digital output is for example connected to GPIO pin D6,
|
|
then if the attribute runTimeD6 is set to 1, the reading runTimeD6 will show for how many seconds the water has been flowing without a stop longer than the
|
|
observation interval specifie in the interval-attribute. This is helpful when you want to create alerts in case someone forgot to close a water tap.<br>
|
|
</ul>
|
|
<br>
|
|
</ul>
|
|
|
|
=end html
|
|
=cut
|
|
|