############################################### # # 47_OBIS.pm # # maintained by gvzdus, however ~95% of code and functions created by # icinger. gvzdus accepts full accountability for the bugs, but the credits # for brilliance go to icinger :-) # # Other credits: # hdgucken, 02/2021: non-blocking extension, optimize ser2net integration # # Original credits by Icinger, main developer until 02/2021: # Thanks to matzefizi for letting me merge this with 70_SMLUSB.pm and for testing # Thanks to immi for testing and supporting help and tips # # $Id$ package main; use strict; use warnings; use Time::HiRes qw(gettimeofday usleep); use Scalar::Util qw(looks_like_number); use POSIX qw{strftime}; use DevIo; use HttpUtils; no warnings 'portable'; # Support for 64-bit ints required my %OBIS_channels = ( "21" =>"power_L1", # Positive active instantaneous power "36" =>"power_L1", # Sum active instantaneous power "41" =>"power_L2", "56" =>"power_L2", "61" =>"power_L3", "76" =>"power_L3", "12" =>"voltage_avg", "32" =>"voltage_L1", "52" =>"voltage_L2", "72" =>"voltage_L3", "11" =>"current_sum", "31" =>"current_L1", "51" =>"current_L2", "71" =>"current_L3", "1.8" =>"total_consumption", "2.8" =>"total_feed", "2" =>"feed_L1", "4" =>"feed_L2", "6" =>"feed_L3", "1" =>"power", # Positive active instantaneous power "15" =>"power", # Absolute active instantaneous power "16" =>"power", # Sum active instantaneous power "24" =>"Gas", ); my %OBIS_codes = ( "Serial" => qr{^0-0:96.1.255(?:.\d+)?\((.*?)\).*}, "Serial" => qr{^(?:1-0:)?0\.0\.[1-9]+(?:.\d+)?\((.*?)\).*}, "Owner" => qr{^1.0.0.0.0(?:.\d+)?\((.*?)\).*}x, "Status" => qr{^1.0.96.5.5(?:.\d+)?\((.*?)\).*}x, "Powerdrops" => qr{^0.0.96.7.\d(?:.\d+)?\((.*?)\).*}, "Time_param" => qr{^0.0.96.2.1(?:.\d+)?\((.*?)\).*}, "Time_current" => qr{^0.0.1.0.0(?:.\d+)?\((.*?)\).*}, "Channel_sum" => qr{^(?:1.0.)?(\d+).1.7(?:.0|.255)?(?:\(.*?\))?\((<|>)?([-+]?\d+\.?\d*)\*?(.*)\).*}, "Channels" => qr{^(?:\d.0.)?(\d+).7\.\d+(?:.0|.255)?(?:\(.*?\))?\((<|>)?([-+]?\d+\.?\d*)\*?(.*)\).*}, "Channels2" => qr{^(?:0.1.)?(\d+).2\.\d+(?:.0|.255)?(?:\(.*?\))?\((<|>)?(-?\d+\.?\d*)\*?(.*)\).*}, "Counter" => qr{^(?:1.\d.)?(\d).(8)\.(\d).(\d+)?(?:\(.*?\))?\((<|>)?(-?\d+\.?\d*)\*?(.*)\).*}, #^(?:1.\d.)?(\d).(8)\.(\d)(?:.\d+)?(?:\(.*?\))?\((<|>)?(-?\d+\.?\d*)\*?(.*)\).* "ManufID" => qr{^129-129:199\.130\.3(?:.\d+)?\((.*?)\).*}, "PublicKey" => qr{^129-129:199\.130\.5(?:.\d+)?\((.*?)\).*}, "ManufID2" => qr{^1.0.96.50.1(?:.\d+)?\((.*?)\).*} ); my %SML_specialities = ( "TIME" => [qr{0.0.96.2.1 |0.0.1.0.0 }x, sub{return strftime("%d-%m-%Y %H:%M:%S", localtime(unpack("i", pack("I", hex(@_)))))}], "HEX2" => [qr{1-0:0\.0\.[0-9] }x, sub{my $a=shift; if ( $a =~ /^[0-9a-fA-F]+$/ ) {$a=~s/(..)/$1-/g;$a=~s/-$//}; return $a;}], "HEX4" => [qr{1.0.96.5.5 |0.0.96.240.\d+ |129.129.199.130.5}x, sub{my $a=shift; if ( $a =~ /^[0-9a-fA-F]+$/ ) {$a=~s/(....)/$1-/g;$a=~s/-$//}; return $a;}], "INFO" => [qr{1-0:0\.0\.[0-9] |129.129.199.130.3}x, ""], ); # Mapping for historical energy consumption, used with 1-0:1.8.0 my %Ext_Channel_Postfix = ( "255" => "", "0" => "", "96" => "_last1d", "97" => "_last7d", "98" => "_last30d", "99" => "_last365d", "100" => "_since_rst" ); my $SML_ENDTAG = chr(0x1B) . chr(0x1B) . chr(0x1B) . chr(0x1B) . chr(0x1A); my $SML_START = chr(0x1B) . chr(0x1B) . chr(0x1B) . chr(0x1B) . chr(0x01) . chr(0x01) . chr(0x01) . chr(0x01); ##################################### sub OBIS_Initialize($) { my ($hash) = @_; $hash->{Match} = "^\/(?s:.*)\!\$"; $hash->{ReadFn} = "OBIS_Read"; $hash->{ReadyFn} = "OBIS_Ready"; $hash->{DefFn} = "OBIS_Define"; $hash->{ParseFn} = "OBIS_Parse"; $hash->{GetFn} = "OBIS_Get"; $hash->{SetFn} = "OBIS_Set"; $hash->{UndefFn} = "OBIS_Undef"; $hash->{AttrFn} = "OBIS_Attr"; $hash->{AttrList}= "do_not_notify:1,0 interval offset_feed offset_energy IODev channels directions alignTime pollingMode:on,off extChannels:on,off,auto unitReadings:on,off ignoreUnknown:on,off valueBracket:first,second,both resetAfterNoDataTime createPreValues:on,off httpAuthorization nohacks ". $readingFnAttributes; } ##################################### sub OBIS_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); return 'wrong syntax: define OBIS devicename@baudrate[,databits,parity,stopbits]|none [MeterType]' if(@a < 3); DevIo_CloseDev($hash); RemoveInternalTimer($hash); my $name = $a[0]; my $dev = $a[2]; my $type = $a[3]//"Unknown"; $hash->{DeviceName} = $dev; $hash->{MeterType}=$type if (defined($type)); my $device_name = "OBIS_".$name; $modules{OBIS}{defptr}{$device_name} = $hash; # If device="none", prepare for an external IO-Module if($dev=~/none|ext/) { if (@a == 4){ my $device_name = "OBIS.".$a[4]; $hash->{CODE} = $a[4]; $modules{OBIS}{defptr}{$device_name} = $hash; } AssignIoPort($hash); Log3 $hash, 1, "OBIS ($name) - OBIS device is none, commands will be echoed only"; return undef; } my $baudrate; my $devi; ($devi, $baudrate) = split("@", $dev); $hash->{helper}{SPEED}="0"; if (defined($baudrate)) { ## added for ser2net connection if($baudrate =~ m/(\d+)(,([78])(,([NEO])(,([012]))?)?)?/) { $baudrate = $1 if(defined($1)); } my %bd=("300"=>"0","600"=>"1","1200"=>"2","2400"=>"3","4800"=>"4","9600"=>"5","18200"=>"6","36400"=>"7","57600"=>"8","115200"=>"9"); $hash->{helper}{SPEED}=$bd{$baudrate}; $hash->{helper}{SPEED2}=$bd{$a[4]//$baudrate}; } else { $baudrate=9600; $hash->{helper}{SPEED}="5"; $hash->{helper}{NETDEV}= $dev =~ /:\d+$/ ? 1 : 0; } my %devs= ( # Name, Init-String, interval, 2ndInit "none" => ["", -1, ""], "Unknown" => ["", -1, ""], "SML" => ["", -1, ""], "Ext" => ["", -1, ""], "Standard" => ["", -1, ""], "VSM102" => ["/?!".chr(13).chr(10), 600, chr(6)."0".$hash->{helper}{SPEED}."0".chr(13).chr(10)], "E110" => ["/?!".chr(13).chr(10), 600, chr(6)."0".$hash->{helper}{SPEED}."0".chr(13).chr(10)], "E350USB" => ["/?!".chr(13).chr(10), 600, chr(6)."0".$hash->{helper}{SPEED}."0".chr(13).chr(10)], "AS1440" => ["/2!".chr(13).chr(10), 600, chr(6)."0".$hash->{helper}{SPEED}."0".chr(13).chr(10)], "MT382" => ["/?!".chr(13).chr(10), 20, chr(0).chr(0).chr(0).chr(0).chr(0).chr(0).chr(6)."0".$hash->{helper}{SPEED}."0".chr(13).chr(10)] #this line added by alkazaa ); if (!$devs{$type}) {return 'unknown meterType. Must be one of , SML, Standard, VSM102, E110, E350USB, AS1440, MT382'}; $devs{$type}[1] = $hash->{helper}{DEVICES}[1] // $devs{$type}[1]; $hash->{helper}{DEVICES} =$devs{$type}; $hash->{helper}{RULECACHE} = {}; $hash->{helper}{TRIGGERTIME}=gettimeofday(); if ($hash->{helper}{DEVICES}[1]>0) { my $t=OBIS_adjustAlign($hash,AttrVal($name,"alignTime",undef),$hash->{helper}{DEVICES}[1]); Log3 $hash, 5, "OBIS ($name) - Internal timer set to ".FmtDateTime($t); InternalTimer($t, "OBIS_GetUpdate", $hash, 0); } InternalTimer(gettimeofday()+60, "OBIS_CheckNoData", $hash, 0) if ($hash->{helper}{NETDEV}); $hash->{helper}{EoM}=-1; Log3 $hash, 5, "OBIS ($name) - Opening device..."; if (! -f $dev) { if ($dev !~ /^https?:\/\/.*/i ) { DevIo_OpenDev($hash, 0, "OBIS_Init", $hash->{helper}{NETDEV} ? "OBIS_Callback" : undef); } else { $hash->{helper}{LASTDATA} = ""; HttpUtils_NonblockingGet({ hash => $hash, header => $hash->{helper}{HTTPAUTH}, url => $dev, callback => \&OBIS_HttpCallback }); } } else { # Debug mode: Open a FHEM debug session and process it... Log3 $hash, 1, "OBIS ($name) - Replaying session"; open (FH, $dev); $attr{$name}{"verbose"} = 5; my $t1 = gettimeofday(); my $lines = 0; while () { next unless /^\d{4}.\d\d.\d\d \d\d:\d\d:\d\d .: OBIS.*Full message->\s*(.*)$/; OBIS_Parse ($hash, (pack 'H*', $1)); $lines++; } my $dt = gettimeofday()-$t1; readingsSingleUpdate($hash, "state", $lines . ' lines in ' . $dt . ' seconds = ' . int($lines/$dt + 0.5) . ' lines per second', 0); close FH; } return; } ##################################### # will be executed if connection establishment fails (see DevIo_OpenDev()) sub OBIS_Callback($$) { my ($hash, $error) = @_; my $name = $hash->{NAME}; # create a log entry with the error message if ($error) { Log3 $name, 4, "OBIS ($name) - error while connecting: $error"; } return; } ##################################### sub OBIS_Get($@) { my ($hash, @a) = @_; my $name = shift @a; my $opt = shift @a; if ($opt eq "update") { OBIS_GetUpdate($hash); } else {return "Unknown argument $opt, choose one of update";} } ##################################### sub OBIS_Set($@) { my ( $hash, @a ) = @_; my $name = shift @a; my $opt = shift @a; my $value = join("", @a); my $teststr=""; my %bd=("300"=>"0","600"=>"1","1200"=>"2","2400"=>"3","4800"=>"4","9600"=>"5","18200"=>"6","36400"=>"7","57600"=>"8","115200"=>"9"); if ($opt eq "setSpeed") { if ($bd{$value} ne $hash->{helper}{SPEED}) { $hash->{helper}{BUFFER}=""; $hash->{helper}{SPEED}=$bd{$value}; print "old Helper: $hash->{helper}{DEVICES}[2] \r\n"; $hash->{helper}{DEVICES}[2]=$hash->{helper}{DEVICES}[2] eq "" ? "" : chr(6)."0".$hash->{helper}{SPEED}."0".chr(13).chr(10); print "new Helper: $hash->{helper}{DEVICES}[2] \r\n"; $hash->{helper}{SpeedChange}=$value; DevIo_SimpleWrite($hash,$hash->{helper}{DEVICES}[0],undef) ; print "Wrote $hash->{helper}{DEVICES}[0]\r\n"; } } if ($opt eq "data") { OBIS_Parse($hash,(pack 'H*', $value)); } return undef; } # Update-Routine sub OBIS_GetUpdate($) { my ($hash) = @_; my $name = $hash->{NAME}; my $dev = $hash->{DeviceName}; my $type= $hash->{MeterType}; RemoveInternalTimer($hash, "OBIS_GetUpdate"); $hash->{helper}{EoM}=-1; if ($hash->{helper}{DEVICES}[1] eq "") {return undef;} if( $init_done ) { if ($dev =~ /^https?:\/\/.*/i ) { HttpUtils_NonblockingGet({ hash => $hash, url => $dev, header => $hash->{helper}{HTTPAUTH}, callback => \&OBIS_HttpCallback }); } else { DevIo_SimpleWrite($hash,$hash->{helper}{DEVICES}[0],undef) ; Log3 $hash, 4, "OBIS ($name) - Wrote $hash->{helper}{DEVICES}[0]"; } } my $t=OBIS_adjustAlign($hash,AttrVal($name,"alignTime",undef),$hash->{helper}{DEVICES}[1]); Log3 ($hash,5,"OBIS ($name) - Internal timer set to ".FmtDateTime($t)) if ($hash->{helper}{DEVICES}[1]>0); InternalTimer($t, "OBIS_GetUpdate", $hash, 0) if ($hash->{helper}{DEVICES}[1]>0); } # Periodic check for dead connections (no data send for some time) sub OBIS_CheckNoData { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash, "OBIS_CheckNoData"); my $t = gettimeofday(); my $maxSilence = AttrVal($name,"resetAfterNoDataTime",90); if ( $hash->{helper}{LastPacketTime} && $hash->{helper}{LastPacketTime} < $t-$maxSilence) { Log3 $hash, 3, "OBIS ($name) - No data received for " . (int ($t-$hash->{helper}{LastPacketTime})) . " seconds, resetting connection"; DevIo_CloseDev($hash); DevIo_OpenDev($hash, 1, "OBIS_Init", $hash->{helper}{NETDEV} ? "OBIS_Callback" : undef); } InternalTimer($t+60, "OBIS_CheckNoData", $hash); } sub OBIS_Init($) { my ($hash) = @_; Log3 $hash, 3, "OBIS ($hash->{NAME}) - Init done"; return undef; } ##################################### sub OBIS_Undef($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); DevIo_CloseDev($hash) if $hash->{DeviceName} ne "none"; return undef; } ##################################### sub OBIS_Read($) { my ($hash) = @_; if( $init_done ) { my $name = $hash->{NAME}; # Log3 $hash, 5, "OBIS ($name) - OBIS_Read called, hash is " . $hash; my $buf = DevIo_SimpleRead($hash); # Log3 $hash, 5, "OBIS ($name) - OBIS_Read gave " . (defined($buf) ? length($buf) : "null"); return if(!defined($buf)); if ( !defined($hash->{helper}{SpeedChange}) || ($hash->{helper}{SpeedChange} eq "")) { # Log3 $hash, 5, "OBIS ($name) - EoM is " . $hash->{helper}{EoM}; OBIS_Parse($hash,$buf) if ($hash->{helper}{EoM}!=1); } else { Log3 $hash, 5, "OBIS ($name) - SpeedChange is " . $hash->{helper}{SpeedChange}; } } return(undef); } my @pow10plus5 = ( 0.00001, 0.0001, 0.001, 0.01, 0.1, 1, 10, 100, 1000, 10000, 100000 ); my %unitmap = ( 0x1E => "Wh", 0x1B => "W", 0x21 => "A", 0x23 => "V", 0x2C => "Hz", 0x01 => "", 0x1D => "varh", ); # Input: devhash, textoutput, reverse byte buffer, maxelements sub OBIS_Parse_List { my $hash = $_[0]; my $elements = $_[2]; my @result; #Log3 $hash, 3, "OBIS_Parse_List : Scan for $elements element"; my $cntdown = $elements; my $isobis = ($elements == 7) ? 1 : 0; while ($cntdown>0 && (length $_[3])) { my $tl = ord chop($_[3]); $isobis &= ($tl == 0x7) if ($cntdown == $elements); #Log3 $hash, 3, "OBIS_Parse_List : TL is " . sprintf("%02X", $tl); my $len = $tl & 0xf; my $tltype = $tl & 0x70; my $tllast = $tl; while ($tllast & 0x80) { $tllast = ord chop($_[3]); if ($tl & 0x70) { Log3 $hash, 3, "2nd TL-byte != 0, reserved according spec"; } $len = ($len*16) + ($tllast & 0xf); } $len--; return undef if ($len>length $_[3] || $len<0); if ($tltype == 0) { # String if ($len<=0) { push @result, ""; } elsif (($len==6) && ($cntdown==$elements)) { push @result, sprintf("%d-%d:%d.%d.%d*%d", ord chop($_[3]), ord chop($_[3]), ord chop($_[3]), ord chop($_[3]), ord chop($_[3]), ord chop($_[3])); } else { my $str; while ($len--) { $str .= sprintf ("%02X", ord chop($_[3])); } if ($elements==7 && $#result==4) { if($result[0]=~$SML_specialities{"HEX4"}[0]) { $str=$SML_specialities{"HEX4"}[1]->($str) } elsif($result[0]=~$SML_specialities{"HEX2"}[0]) { $str=$SML_specialities{"HEX2"}[1]->($str) } else { $str=~s/([A-F0-9]{2})/chr(hex($1))/eg; $str=~s/[[:cntrl:]]//g; $str=~s/[^!-~\s\r\n\t]//g; } } push @result, $str; } } elsif ($tltype == 0x50 || $tltype == 0x60) { # Signed (5) or Unsigned (6) Int my $num = 0; my $subme = 0; if ($tltype==0x50 && $len<4 && $isobis && $cntdown==2 && $result[0]=~/^1-0:[1357]6\.7\.0/ && $hash->{helper}{DZGHACK}) { $tltype = 0x60; } if ($len==8) { $num = unpack($tltype = 0x50 ? 'q>' : 'Q>', chop($_[3]).chop($_[3]).chop($_[3]).chop($_[3]). chop($_[3]).chop($_[3]).chop($_[3]).chop($_[3])); } elsif ($len==4) { $num = unpack($tltype = 0x50 ? 'l>' : 'L>', chop($_[3]).chop($_[3]).chop($_[3]).chop($_[3])); } elsif ($len<8) { $num = ord chop($_[3]) if ($len--); $subme = ($tltype == 0x50) && ($num & 0x80) ? ( 1 << (8*($len+1)) ) : 0; while ($len-- > 0) { $num = ($num<<8) | ord chop($_[3]); } } else { Log3 $hash, 1, "OBIS - Number sized over 8 bytes - no support implemented yet"; } push @result, ($num-$subme); } elsif ($tltype == 0x40) { push @result, (ord chop($_[3]) != 0); } elsif ($tltype == 0x70) { push @result, ref OBIS_Parse_List($hash, $_[1], $len+1, $_[3]); } $cntdown--; } my $i = 0; return @result if (!$isobis); # We should have now an 7 element array: reading(0), status(1), valTime(2), unit(3), scaler(4), data(5) if ($result[4] && looks_like_number($result[4]) && looks_like_number($result[5])) { $result[5] *= ($result[4]<-5 || $result[4]>5) ? 10**$result[4] : $pow10plus5[$result[4]+5]; } if (looks_like_number($result[3])) { my $repl = $unitmap{$result[3]}; $result[3] = $repl // "var"; } if (looks_like_number($result[1])) { my $direction = ""; # Direction: Bit 5 Version 1.0, Bit 11 later if (($result[1] & 0xE2) == 0xA2 || ($result[1] & 0x807) == 0x804) { $direction = "<"; } elsif (($result[1] & 0xE2) == 0x82 || ($result[1] & 0x807) == 0x004) { $direction = ">"; } $result[5] = $direction . $result[5]; $hash->{helper}{DIRECTIONSUM} = $direction; } if ($result[0]=~/^1-0:16\.7\.0/ && $hash->{helper}{HLYHACK} && $result[5]>0 && $hash->{helper}{DIRECTIONSUM} eq "<") { $result[5] = -$result[5]; } my $line = $result[0] . "(" . $result[5] . ($result[3] eq "" ? "" : "*".$result[3]) . ")\r\n"; if ($line=~/^1-0:96\.50\.1\*.*\((DZG|HLY)/) { $hash->{helper}{$1 . "HACK"} = ! defined $hash->{helper}{NOHACKS}; } $_[1] .= $line; return undef; # Log3 $hash,3,"OBIS line is " . $line . $line2 . ")"; # $line.$line2.")\r\n" # while ($i < scalar @result) { # Log3 $hash,3,"List element $i is " . $result[$i]; # $i++; # } } # Input: devhash, bytesequence # Output: decoded text, remaining buffer sub OBIS_trySMLdecode($$) { my ( $hash, $remainingSML ) = @_; my $name = $hash->{NAME}; my $ll = AttrVal($name, "verbose", 3); my $t=$remainingSML; if ($remainingSML=~m/SML\((.*)\)/g) { $remainingSML=$1 }; return $remainingSML if($remainingSML!~/[\x00-\x09|\x10-\x1F]/g); $hash->{MeterType}="SML"; # Fast fail for messages w/o end tag my $newMsg=""; do { # 1) endTag has to be found, 2) startTag has to be found, 3) end behind start: my $endtagIdx = index ($remainingSML, $SML_ENDTAG); return ("", $remainingSML) if ($endtagIdx<0 || ((length $remainingSML)-$endtagIdx) < 8); my $startIdx = index ($remainingSML, $SML_START); return ("", substr($remainingSML, $endtagIdx+8)) if ($startIdx<0); return ("", substr($remainingSML, $startIdx)) if ($startIdx>=$endtagIdx); my $msg=substr($remainingSML, $startIdx, $endtagIdx+8-$startIdx); Log3 $hash,5,"OBIS ($name) - SML-Parse " . uc(unpack('H*',$msg)) if ($ll>=5); if (OBIS_CRC16($hash,$msg) == 1) { $remainingSML=""; #reset possible further messages if actual CRC ok; if someone misses some messages, we remove it. my $OBISmsg=""; my $initstr="/"; Log3 $hash,5,"OBIS ($name) - Full message-> " . uc(unpack('H*',$msg)) if ($ll>=5); OBIS_Parse_List($hash, $newMsg, 9999, scalar reverse substr($msg, 8, $endtagIdx-8)); $initstr=~s/\\$//; $newMsg=$initstr.chr(13).chr(10).$newMsg; $newMsg.="!".chr(13).chr(10); Log3 $hash,4,"OBIS ($name) - MSG IS: \r\n$newMsg" if ($ll>=4); } else { $hash->{CRC_Errors}+=1; Log3 $hash,4,"OBIS ($name) - CRC Error in Input" if ($ll>=4); $remainingSML = substr($remainingSML, $endtagIdx+8); } } while (length ($remainingSML)>4); return ($newMsg,$remainingSML); } sub OBIS_HttpCallback($$$) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; if ($err) { Log3 $hash,1,"OBIS ($name) - Error $err"; } else { Log3 $hash,5,"OBIS ($name) - Got data, len " . length $data; if ($hash->{helper}{LASTDATA} eq $data) { Log3 $hash,4,"OBIS ($name) - HTTP data unchanged"; } else { $hash->{helper}{LASTDATA} = $data; OBIS_Parse($hash, $data); } } } sub OBIS_Parse($$) { my ($hash, $buf) = @_; my $name = $hash->{NAME}; # gvz Unsure what this is for # my $buf2=uc(unpack('H*',$buf)); # if($hash->{MeterType}!~/SML|Unknown/ && $buf2=~m/7701([0-9A-F]*?)01/g) { # Log3 $hash, 3, "OBIS ($name) - OBIS_Ext called"; # my (undef,undef,$OBISid,undef)=OBIS_decodeTL($1); # # my $device_name = "OBIS.".$OBISid; # Log3 $hash,5,"OBIS ($name) - New Devicename: $device_name"; # my $def = $modules{OBIS}{defptr}{"$device_name"}; # if(!$def) { # Log3 $hash, 3, "OBIS ($name) - Unknown device $device_name, please define it"; # return "UNDEFINED $device_name OBIS none Ext $OBISid"; # } # } $hash->{helper}{BUFFER} .= $buf; if (length($hash->{helper}{BUFFER}) >10000) { #longer than 3 messages, this is a traffic jam $hash->{helper}{BUFFER} =substr( $hash->{helper}{BUFFER} , -10000); } my %dir=("<"=>"out",">"=>"in"); my $buffer=$hash->{helper}{BUFFER}; my $remainingSML; ($buffer,$remainingSML) = OBIS_trySMLdecode($hash,$buffer) if ($hash->{MeterType}=~/SML|Ext|Unknown/); my $type= $hash->{MeterType}; $buf='/'.$buf; $buf =~ /!((?!\/).*)$/gmsi; $buf=$1; my $ll = AttrVal($name, "verbose", 3); my $crlfPos = index($buffer,chr(13).chr(10)); if($crlfPos > -1){ readingsBeginUpdate($hash); my $ignoreUnknownOff = AttrVal($name,"ignoreUnknown","off") eq "off"; my $unitReadingsOff = AttrVal($name,"unitReadings","off") eq "off"; my $extChannels = AttrVal($name,"extChannels","auto"); my $ruleCache = $hash->{helper}{RULECACHE}; while($crlfPos > -1) { my $rmsg=""; $rmsg = substr($buffer, 0, $crlfPos); Log3 $hash,5,"OBIS ($name) - Msg-Parse: $rmsg" if ($ll>=5); my $channel=" "; if($rmsg=~/\/.*|^((?:\d{1,3}-\d{1,3}:)?(?:\d{1,3}|[CF]).\d{1,3}(?:.\d{1,3})?(?:\*\d{1,3})?)(?:\(.*?\))?\(.*?\)|!/) { # old regex: \/.*|\d-\d{1,3}:\d{1,3}.\d{1,3}.\d{1,3}\*\d{1,3}\(.*?\)|! if (length $1) { $channel=$1; $channel=~tr/:*-/.../; # Log 3,"Channel would be: $channel"; } if ($hash->{MeterType} eq "Unknown") {$hash->{MeterType}="Standard"} # End of Message if ($rmsg=~/^!.*/) { $hash->{helper}{EoM}+=1 if (($hash->{helper}{DEVICES}[1] // 0)>0); } #Version elsif ($rmsg=~ /.*\/(.*)/) { DevIo_SimpleWrite($hash,$hash->{helper}{DEVICES}[2],undef) if (!$hash->{helper}{DEVICES}[2] eq ""); if (ReadingsVal($name,"Version","") ne $1) {readingsBulkUpdate($hash, "Version" ,$1); } $hash->{helper}{EoM}=0; } elsif ($hash->{helper}{EoM}!=1) { my $found=0; # Check superseeding settings in channels attribute: if (!($hash->{helper}{Channels}{$channel} //$hash->{helper}{Channels}{$1})) { my $cache_code = $rmsg; $cache_code =~ s/\(.*$//; my $code = $ruleCache->{$cache_code}; # Log3 $hash,3,"OBIS ($name) - Cache result for " . $cache_code . " is " . $code; if (! defined $code) { for my $c (keys %OBIS_codes) { if ($rmsg =~ $OBIS_codes{$c}) { $ruleCache->{$cache_code} = $c; Log3 $hash,4,"OBIS ($name) - Storing $c for $cache_code in Cache"; $hash->{helper}{RULECACHE} = $ruleCache; $code = $c; } } } if (defined $code && $code ne "unknown") { if ($rmsg =~ $OBIS_codes{$code}) { Log3 $hash,5,"OBIS ($name) - Msg $rmsg is of type $code" if ($ll>=5); if (rindex ($code, "Channel_sum", 0) eq 0) { my $L= $hash->{helper}{Channels}{$channel} //$hash->{helper}{Channels}{$1} // "sum_$OBIS_channels{$1}" //$channel; if ($ignoreUnknownOff || $L ne $channel) { readingsBulkUpdate($hash, $L,(looks_like_number($3) ? $3+0 : $3).($unitReadingsOff?"":" $4")); readingsBulkUpdate($hash, "dir_$L",$hash->{helper}{directions}{$2} // $dir{$2}) if (length $2); } } elsif (rindex ($code, "Channels", 0) eq 0) { my $L=$hash->{helper}{Channels}{$channel} //$hash->{helper}{Channels}{$1} // $OBIS_channels{$1} //$channel; if ($ignoreUnknownOff || $L ne $channel) { readingsBulkUpdate($hash, "$L",(looks_like_number($3) ? $3+0 : $3).($unitReadingsOff?"":" $4")); readingsBulkUpdate($hash, "dir_$L",$hash->{helper}{directions}{$2} // $dir{$2}) if (length $2); } } elsif (rindex ($code, "Counter", 0) eq 0) { my $L=$hash->{helper}{Channels}{$channel} //$hash->{helper}{Channels}{$1.".".$2} // $OBIS_channels{$1.".".$2} // $channel; my $chan=$3+0 > 0 ? "_Ch$3" : ""; if ($extChannels eq "auto" && defined $4) { my $ecp = $Ext_Channel_Postfix{$4}; $chan.= $ecp // ".$4"; } elsif ($extChannels eq "on") { $chan.=".$4" if $4; } if ($ignoreUnknownOff || $L ne $channel) { if($1==1) { Log3($hash,4,"OBIS ($name) - Set ".$L.$chan." to ".((looks_like_number($3) ? $6+0 : $5) +AttrVal($name,"offset_energy",0))) if ($ll>=4); readingsBulkUpdate($hash, $L.$chan ,(looks_like_number($3) ? $6+0 : $5) +AttrVal($name,"offset_energy",0).($unitReadingsOff?"":" $7")); } elsif ($1==2) { readingsBulkUpdate($hash, $L.$chan ,(looks_like_number($3) ? $6+0 : $5) +AttrVal($name,"offset_feed",0).($unitReadingsOff?"":" $7")); } readingsBulkUpdate($hash, "dir_$L",$hash->{helper}{directions}{$4} // $dir{$4}) if (length $4); } } else { my $data=$1; if (rindex ($code, "Time", 0) eq 0) { if($rmsg=~$SML_specialities{"TIME"}[0]) { $data=~/(\d+)/; $data=$SML_specialities{"TIME"}[1]->($1) } } elsif ($code eq "Status" || $code eq "PublicKey") { if ($rmsg=~$SML_specialities{"HEX4"}[0]) { $data=$SML_specialities{"HEX4"}[1]->($data) } } elsif ($code eq "Owner" || $code eq "Serial") { $data=$SML_specialities{"HEX2"}[1]->($data) } my $userChannelName = $hash->{helper}{Channels}{$channel}; $code = $userChannelName if (defined $userChannelName); if (defined $1) { $userChannelName = $hash->{helper}{Channels}{$1}; $code = $userChannelName if (defined $userChannelName); } readingsBulkUpdate($hash, $code ,$data); } $found=1; } } else { if (! $code) { Log3 $hash,3,"OBIS ($name) - Unknown Message: $rmsg"; $ruleCache->{$cache_code} = "unknown"; $hash->{helper}{RULECACHE} = $ruleCache; } } } if ($found==0) { # Log 3,"Found a Channel-Attr"; $rmsg=~/^((?:\d{1,3}-\d{1,3}:)?(?:\d{1,3}|[CF]).\d{1,3}(?:.\d{1,3})?(?:\*\d{1,3})?)(?:\((.*?)\))?\((.*?)\)/; my $chan=$hash->{helper}{Channels}{$channel} //$hash->{helper}{Channels}{$1} // $OBIS_channels{$1} //$channel; my $chan1=$chan; my $chan2=$chan."_2"; # Log 3,"Setting $chan"; my $v1=$3; my $v2; my $valueBracket = AttrVal($name,"valueBracket","second"); if ($valueBracket eq "first") { $v1=length $2 ? $2 : $3; } elsif ($valueBracket eq "both") { $v2=$2; ($v1,$v2)=($v2,$v1); if (!length $v1 and length $v2) {$v1=$v2;$v2=""} $chan1.="_1" if length $2; } if ($unitReadingsOff) { $v1=~s/(.*)\*.*/$1/; if ($v2) {$v2=~s/(.*)\*.*/$1/}; } $v1+=0 if (looks_like_number($v1)); $v2+=0 if (looks_like_number($v2)); if ($ignoreUnknownOff || $chan ne $channel) { readingsBulkUpdate($hash, $chan1 ,$v1) if length $v1; readingsBulkUpdate($hash, $chan2 ,$v2) if length $v2; } } # } } if ($hash->{helper}{EoM}==1) {last;} } $buffer = substr($buffer, $crlfPos+2); $crlfPos = index($buffer,chr(13).chr(10)); } readingsEndUpdate($hash,1); $hash->{helper}{LastPacketTime} = gettimeofday; } if (defined($remainingSML)) {$hash->{helper}{BUFFER}=$remainingSML} else{$hash->{helper}{BUFFER}=$buffer;} if($hash->{helper}{EoM}==1) { $hash->{helper}{BUFFER}="";} return $name; } ##################################### sub OBIS_Ready($) { my ($hash) = @_; return DevIo_OpenDev($hash, 1, "OBIS_Init", $hash->{helper}{NETDEV} ? "OBIS_Callback" : undef) if($hash->{STATE} eq "disconnected"); my $name = $hash->{NAME}; my $dev=$hash->{DeviceName}; # This is relevant for windows/USB only my $po = $hash->{USBDev}; my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags); return if (!$po); ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; return ($InBytes>0); } sub OBIS_Attr(@) { my ($cmd,$name,$aName,$aVal) = @_; # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value my $hash = $defs{$name}; my $dev=$hash->{DeviceName}; $dev =~ s/\@.*$//g; #{$hash->{"test"}->{SetFn} = "OBIS_Set"} if ($cmd eq "del") { if ($aName eq "channels") { $hash->{helper}{Channels}=undef;} if ($aName eq "pollingMode" || $aName eq "interval") { RemoveInternalTimer($hash,"OBIS_GetUpdate"); $hash->{helper}{DEVICES}[1]=0; $hash->{helper}{EoM}=-1; # delete($readyfnlist{"$name.$dev"}); # $selectlist{"$name.$dev"} = $hash; # DevIo_CloseDev($hash); # DevIo_OpenDev($hash, 0, "OBIS_Init"); } } if ($cmd eq "set") { if ($aName eq "channels") { $hash->{helper}{Channels}=eval $aVal; if ($@) { Log3 $name, 3, "OBIS ($name) - X: Invalid regex in attr $name $aName $aVal: $@"; $hash->{helper}{Channels}=undef; } } if ($aName eq "directions") { $hash->{helper}{directions}=eval $aVal; if ($@) { Log3 $name, 3, "OBIS ($name) - X: Invalid regex in attr $name $aName $aVal: $@"; $hash->{helper}{directions}=undef; } } if ($aName eq "interval" || $aName eq "pollingMode") { return "OBIS ($name) - $name: attr interval must be a number -> $aVal" if ($aName eq "interval" && $aVal!~/^[1-9][0-9]*$/); return "OBIS ($name) - $name: attr pollingMode must be on or off" if ($aName eq "pollingMode" && $aVal!~/^(on|off)$/); my $dopoll = ($aName eq "interval" || AttrVal($name,"interval",0)>0) && (($aName eq "pollingMode" && $aVal eq "on") || ($aName ne "pollingMode" && AttrVal($name,"pollingMode","off") eq "on")); Log3 $name, 3, "OBIS ($name) - Attr $aName Val $aVal, dopoll = $dopoll"; if ($dopoll) { # DevIo_CloseDev($hash); # delete $hash->{FD}; # delete($selectlist{"$name.$dev"}); # $readyfnlist{"$name.$dev"} = $hash; $hash->{helper}{TRIGGERTIME}=gettimeofday(); RemoveInternalTimer($hash,"OBIS_GetUpdate"); $hash->{helper}{DEVICES}[1]=($aName eq "interval" ? $aVal : AttrVal($name,"interval",0)); my $t=OBIS_adjustAlign($hash,AttrVal($name,"alignTime",undef),$hash->{helper}{DEVICES}[1]); Log3 ($hash,5,"OBIS ($name) - Internal timer set to ".FmtDateTime($t)) if ($hash->{helper}{DEVICES}[1]>0); InternalTimer($t, "OBIS_GetUpdate", $hash, 0) if ($hash->{helper}{DEVICES}[1]>0); $hash->{helper}{EoM}=1; } else { RemoveInternalTimer($hash,"OBIS_GetUpdate"); # delete($readyfnlist{"$name.$dev"}); $hash->{helper}{DEVICES}[1]=0; # $selectlist{"$name.$dev"} = $hash; # DevIo_CloseDev($hash); # DevIo_OpenDev($hash, 0, "OBIS_Init"); $hash->{helper}{EoM}=-1; } } if ($aName eq "alignTime") { if ($hash->{helper}{DEVICES}[1]>0 || !$init_done) { if ($aVal=~/\d+/) { RemoveInternalTimer($hash,"OBIS_GetUpdate"); $hash->{helper}{TRIGGERTIME}=gettimeofday(); my $t=OBIS_adjustAlign($hash,$aVal,$hash->{helper}{DEVICES}[1]); Log3 ($hash,5,"OBIS ($name) - Internal timer set to ".FmtDateTime($t)); InternalTimer($t, "OBIS_GetUpdate", $hash, 0); } else {return "OBIS ($name): attr alignTime must be a Value >0"} } else { if ($init_done) { return "OBIS ($name): attr alignTime is useless, if no interval is specified"; } } } if ($aName eq "httpAuthorization") { return "OBIS ($name) - $name: attr httpAuthorization must be in the format user:password, e.g. for Tibber Pulse admin:ABCD-1234" if ($aVal!~/^\w+:.*$/); $hash->{helper}{HTTPAUTH} = "Authorization: Basic " . encode_base64($aVal); } if ($aName eq "nohacks") { $hash->{helper}{NOHACKS} = $aVal; } } return undef; } sub OBIS_adjustAlign($$$) { my($hash, $attrVal, $interval) = @_; if (!$attrVal) {return gettimeofday()+$interval;} my ($alErr, $alHr, $alMin, $alSec, undef) = GetTimeSpec($attrVal); return "$hash->{NAME} alignTime: $alErr" if($alErr); my $tspec=strftime("\%H:\%M:\%S", gmtime($interval)); # Obis_adjustAlignTimetest2($hash,AttrVal($hash->{NAME},"alignTime",undef),$hash->{helper}{DEVICES}[1]); my (undef, $hr, $min, $sec, undef) = GetTimeSpec($tspec); my $now = time(); my $step = ($hr*60+$min)*60+$sec; my $alTime = ($alHr*60+$alMin)*60+$alSec;#-fhemTzOffset($now); my $ttime = int($hash->{helper}{TRIGGERTIME}); my $off = ($ttime % 86400) - 86400; if ($off >= $alTime) { $ttime = gettimeofday();#-86400; $off -= 86400; } my $off2=$off; while($off < $alTime) { $off += $step; } $ttime += ($alTime-$off); $ttime += $step if($ttime <= $now); $hash->{NEXT} = FmtDateTime($ttime); $hash->{helper}{TRIGGERTIME} = ($off2<=$alTime) ? $ttime : (gettimeofday()); return $hash->{helper}{TRIGGERTIME}; } sub OBIS_hex2int { my ($hexstr) = @_; return 0 if $hexstr !~ /^[0-9A-Fa-f]{1,35}$/; my $num = hex($hexstr); return $num >> 31 ? $num - 2 ** 32 : $num; } sub OBIS_CRC16($$) { my ($hash,$buff)=@_; my @crc16 = ( 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78); my $crc=0xFFFF; my $a=reverse substr($buff,0,-2); my $b=substr($buff,-2); my $crc2=OBIS_hex2int(uc(unpack('H*',$b))); while (length $a) { $crc = ($crc >> 8) ^ $crc16[($crc ^ ord(chop $a)) & 0xff]; } $crc ^= 0xffff; $crc = (($crc & 0xff) << 8) | (($crc & 0xff00) >> 8); return $crc2==$crc ? 1 : 0; } 1; =pod =item device =item summary Collects data from Smartmeters that report in OBIS-Standard =item summary_DE Wertet Smartmeter aus, welche ihre Daten im OBIS-Standard senden =begin html

OBIS

    This module is for SmartMeters, that report their data in OBIS-Standard. It doesn't matter, whether the data comes as PlainText or SML-encoded.

    Define
      define <name> OBIS device|none [MeterType]

      <device> specifies the serial port or hostname/ip-address:port to communicate with the smartmeter. For Tibber Pulse and requesting via HTTP, specify the URL in the LAN. In case of a serial device and with Linux:
      • the device will be named /dev/ttyUSBx, where x is a number.
      • or - to avoid wrong numbering on server reboots - use the ID of the USB device, e.g. /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A106Q3OW-if00-port0
      You may specify the baudrate used after the @ char, 9600 is common

      If you use a ser2net connection, <device> is ip-address:port or hostname:port.

      Optional: MeterType can be of
      • VSM102 -> Voltcraft VSM102
      • E110 -> Landis&&;Gyr E110
      • E350USB -> Landis&&;Gyr E350 USB-Version
      • MT382 -> ISKRA MT382
      • Standard -> Data comes as plainText
      • SML -> Smart Message Language

      Example:
      define myPowerMeter OBIS /dev/ttyPlugwise@9600,7,E,1 VSM102
      define myPowerMeter OBIS 192.168.0.12:1234 Standard

    Attributes
    • offset_feed
      offset_energy

      If your smartmeter is BEHIND the meter of your powersupplier, then you can hereby adjust the total-reading of your SM to that of your official one.
    • channels
      With this, you can rename the reported channels.
      e.g.: attr myOBIS channels {"1.0.96.5.5.255"=>"Status","1.0.0.0.0.255"=>"Info","16.7"=>"Verbrauch"}
    • directions
      Some Meters report feeding/comnsuming of power in a statusword. If this is set, you get an extra reading dir_total_consumption which defaults to "in" and "out".
      Here, you can change this text with, e.g.: attr myOBIS directions {">" => "pwr consuming", "<"=>"pwr feeding"}
    • extChannels
      Possible values:
      auto (default). The values 0 and 255 will give the base counter name like with off. Values from 96 to 100 add a postfix "_last1d" / "_last7d", "_last30d", "_last365d" and "_since_rst" to the reading. Other values lead to results like "on".
      off Historical values are not considered and might overwrite current values.
      on Every counter-reading gets appended by the extChannel-number, e.g. total_consumption becomes total_consumption.255
    • interval
      The polling-interval in seconds. (Only useful in Polling-Mode)
    • alignTime
      Aligns the intervals to a given time. Each interval is repeatedly calculated. So if alignTime=00:00 and interval=600 aligns the interval to xx:00:00, xx:10:00, xx:20:00 etc....
    • pollingMode
      Changes from direct-read to polling-mode. Useful with meters, that send a continous datastream. Reduces CPU-load.
    • unitReadings
      Adds the units to the readings like w, wH, A etc.
    • valueBracket
      Sets, if to use the value from the first or the second bracket, if applicable. Standard is "second"
    • resetAfterNoDataTime
      If on a TCP-connection no data was received for the given time, the connection is closed and reopened
    • httpAuthorization
      If the SML data is fetched by HTTP (Usecase: e.g. Tibber Pulse), this attribute is used to authenticate the HTTP-request. The format must be :, e.g. for Tibber Pulse something like "admin:"
    • nohacks
      For DZG- and HOLY-Meters a workaround was implemented to avoid certain bugs in encoding. Latest DZG meters seem to have fixed the bug. By setting "nohacks" to "1", the workarounds are disabled.

=end html =begin html_DE

OBIS

    Modul für Smartmeter, die ihre Daten im OBIS-Standard senden. Hierbei ist es egal, ob die Daten als reiner Text oder aber SML-kodiert kommen.

    Define
      define <name> OBIS device|none [MeterType]

      <device> gibt den seriellen Port oder den Hostnamen/die IP-Adresse:Port an. Für den Tibber Pulse ist stattdessen die vollständige URL im LAN anzugeben, unter der die Daten geladen werden können.
      Bei seriellem Port (USB) und Linux gibt man hier entweder
      • als Gerät etwas wie /dev/ttyUSBx, an (x eine Zahl)
      • oder - um nach einem Neustart der Hardware keine geänderte Reihenfolge zu riskieren - sucht man die passende ID unter /dev/serial/by-id/, also z.B. /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_A106Q3OW-if00-port0


      Optional: MeterType kann sein:
      • VSM102 -> Voltcraft VSM102
      • E110 -> Landis&&;Gyr E110
      • E350USB -> Landis&&;Gyr E350 USB-Version
      • MT382 -> ISKRA MT382
      • Standard -> Daten kommen als plainText
      • SML -> Smart Message Language

      Beispiel:
      define myPowerMeter OBIS /dev/ttyPlugwise@9600,7,E,1 VSM102
      define myPowerMeter OBIS 192.168.0.12:1234 Standard

    Attribute
    • offset_feed
      offset_energy

      Wenn das Smartmeter hinter einem Zähler des EVU's sitzt, kann hiermit der Zähler des Smartmeters an den des EVU's angepasst werden.
    • channels
      Hiermit können die einzelnen Kanal-Readings mittels RegExes umbenannt werden.
      Beispiel: attr myOBIS channels {"1.0.96.5.5.255"=>"Status","1.0.0.0.0.255"=>"Info","16.7"=>"Verbrauch"}
    • directions
      Manche SmartMeter senden im Statusbyte die Stromrichtung. In diesem Fall gibt es ein extra Reading "dir_total_consumption" welches standardmäßig "in" and "out" beinhaltet
      Hiermit kann dieser Text geändert werden, z.B.: attr myOBIS directions {">" => "pwr consuming", "<"=>"pwr feeding"}
    • extChannels
      Mögliche Werte:
      auto (default). Die Werte 0 und 255 werden wie bei extChannels off geschrieben. Die Werte 96-100 führen zu Postfix "_last1d" / "_last7d", "_last30d", "_last365d" und "_since_rst". Andere Werte werden wie bei "on" geschrieben.
      off Historische Werte werden nicht berücksichtigt und überschreiben ggf. aktuelle Werte.
      on Jedem Counter-Wert wird die extChannel-Nummer angehängt, aus total_consumption wird z.B. total_consumption.255
    • interval
      Abrufinterval der Daten. (Bringt nur im Polling-Mode was)
    • alignTime
      Richtet den Zeitpunkt von nach einer bestimmten Uhrzeit aus.
    • pollingMode
      Hiermit wird von Direktbenachrichtigung auf Polling umgestellt. Bei Smartmetern, welche von selbst im Sekundentakt senden, kann das zu einer spürbaren Senkung der Prozessorleistung führen.
    • unitReadings
      Hängt bei den Readings auch die Einheiten an, zB w, wH, A usw.
    • valueBracket
      Legt fest, ob der Wert aus dem ersten oder zweiten Klammernpaar genommen wird. Standard ist "second"
    • resetAfterNoDataTime
      Bei TCP-Verbindungen wird nach der angegebenen Zahl Sekunden die Verbindung geschlossen und neu geöffnet, wenn keine Daten empfangen wurden
    • httpAuthorization
      Werden die SML-Daten per HTTP abgefragt (Usecase: Tibber Pulse z.B.), wird hiermit die Authentifizierung für den HTTP-Request gesetzt. Das Format muss : sein, also für den Tibber Pulse i.d.R. "admin:"
    • nohacks
      Für DZG- und HOLY-Zähler wurden jeweils Workarounds für Bugs im SML-Encoding implementiert. Für neue DZG-Zähler scheint der Bug behoben zu sein. Mit "nohacks" auf "1" werden die Workarounds abgeschaltet.

=end html_DE =cut