From 4d16d1c165d6c8d8aba09478377f9a237183102b Mon Sep 17 00:00:00 2001 From: fhemmiv <> Date: Thu, 29 Jun 2017 06:03:03 +0000 Subject: [PATCH] 00_SmartMeterP1:New module in fhem repository git-svn-id: https://svn.fhem.de/fhem/trunk@14594 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/00_SmartMeterP1.pm | 627 +++++++++++++++++++++++++++++++++++ 1 file changed, 627 insertions(+) create mode 100755 fhem/FHEM/00_SmartMeterP1.pm diff --git a/fhem/FHEM/00_SmartMeterP1.pm b/fhem/FHEM/00_SmartMeterP1.pm new file mode 100755 index 000000000..1b57680bd --- /dev/null +++ b/fhem/FHEM/00_SmartMeterP1.pm @@ -0,0 +1,627 @@ +############################################## +# $Id$ +# +# Version: 1.8 +# Date: 2017-06-29 +# Corrected documentation. +# +# Version: 1.7 +# Date: 2015-12-29 +# Updated documentation for new attribute +# +# Version: 1.6 +# Date: 2015-12-29 +# Added attribute to select if leading zero's are trimmed off +# +# Version: 1.5 +# Date: 2015-12-28 +# Added extra values +# +# Version: 1.4 +# Date: 2015-12-28 +# Fixed problem where wilcard was not removed and +# added power returned values +# +# Version: 1.3 +# Date: 2015-12-24 +# Removed the wildcard between value and unit +# +# Version: 1.2 +# Date: 2015-12-23 +# Added code to handle other protocol from smartmeter. (DSMR P1) + +package main; + +use strict; +use warnings; +use Time::HiRes qw(gettimeofday); +use Data::Dumper; +use DBI; + +sub SmartMeterP1_Attr(@); +sub SmartMeterP1_ParseTelegramLine($$$@); +sub SmartMeterP1_Parse($$$$); +sub SmartMeterP1_Read($); +sub SmartMeterP1_Ready($); + +sub +SmartMeterP1_Initialize($) +{ + my ($hash) = @_; + + require "$attr{global}{modpath}/FHEM/DevIo.pm"; + +# Provider + $hash->{ReadFn} = "SmartMeterP1_Read"; + $hash->{ReadyFn} = "SmartMeterP1_Ready"; + +# Normal devices + $hash->{DefFn} = "SmartMeterP1_Define"; + $hash->{UndefFn} = "SmartMeterP1_Undef"; + $hash->{AttrFn} = "SmartMeterP1_Attr"; + $hash->{AttrList}= "dbName dbHost dbPort dbUser dbPassword write2db:1,0 dbUpdateInterval:0,1,5,10,15,20,25,30,45,60 removeUnitSeparator:false,true removeLeadingZero:false,true " . + $readingFnAttributes; + + $hash->{ShutdownFn} = "SmartMeterP1_Shutdown"; + +} + +##################################### +sub +SmartMeterP1_Define($$) +{ + my ($hash, $def) = @_; + my @a = split("[ \t][ \t]*", $def); + + if(@a != 3) { + my $msg = "wrong syntax: define SmartMeterP1 {none | devicename[\@baudrate]} "; + Log3 undef, 2, $msg; + return $msg; + } + + DevIo_CloseDev($hash); + + my $name = $a[0]; + my $dev = $a[2]; + + if($dev eq "none") { + Log3 $name, 1, "$name device is none, will use deault /dev/ttyUSB0@115200"; + } + + $hash->{DeviceName} = $dev; + my $ret = DevIo_OpenDev($hash, 0, "SmartMeterP1_DoInit"); + + $hash->{Telegram} = {}; + $hash->{TelegramStart} = 0; + + $hash->{DBH} = undef; + + return $ret; +} + +##################################### +sub +SmartMeterP1_Undef($$) +{ + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + foreach my $d (sort keys %defs) { + if(defined($defs{$d}) && + defined($defs{$d}{IODev}) && + $defs{$d}{IODev} == $hash) + { + my $lev = ($reread_active ? 4 : 2); + Log3 $name, $lev, "deleting port for $d"; + delete $defs{$d}{IODev}; + } + } + + DevIo_CloseDev($hash); + + $hash->{DBH}->disconnect if (defined($hash->{DBH})); + + return undef; +} + +##################################### +sub +SmartMeterP1_Shutdown($) +{ + my ($hash) = @_; + return undef; +} + +##################################### +sub +SmartMeterP1_DoInit($) +{ + my $hash = shift; + my $name = $hash->{NAME}; + my $err; + my $msg = undef; + + readingsSingleUpdate($hash, "state", "Initialized", 1); + + return undef; +} + +##################################### +# called from the global loop, when the select for hash->{FD} reports data +sub +SmartMeterP1_Read($) +{ + my ($hash) = @_; + + my $buf = DevIo_SimpleRead($hash); + return "" if(!defined($buf)); + my $name = $hash->{NAME}; + + my $culdata = $hash->{PARTIAL}; + Log3 $name, 5, "SmartMeterP1/RAW: $culdata/$buf"; + $culdata .= $buf; + + while($culdata =~ m/\n/) { + my $rmsg; + ($rmsg,$culdata) = split("\n", $culdata, 2); + $rmsg =~ s/\r//; + SmartMeterP1_Parse($hash, $hash, $name, $rmsg) if($rmsg); + } + $hash->{PARTIAL} = $culdata; +} + +sub +ConvertTelegramTime($) +{ + my $inTime = shift; + + $inTime =~ s/(..)(..)(..)(..)(..)(..)[SW]*/20$1\-$2\-$3 $4\:$5\:$6/; + + return $inTime; +} + +sub +SmartMeterP1_Connect2DB($$$) +{ + my ($hash,$name,$write2db) = @_; + + if ($write2db == 1) { + if (defined($hash->{DBH})) { + $hash->{DBH}->disconnect; + } + + my $dbHost = AttrVal($name,"dbHost",undef); + my $dbPort = AttrVal($name,"dbPort",3306); + my $dbName = AttrVal($name,"dbName",undef); + my $dbUser = AttrVal($name,"dbUser",undef); + my $dbPassword = AttrVal($name,"dbPassword",undef); + + Log3 $name, 5, "SmartMeterP1:Connecting to DBI:mysql:database=$dbName;host=$dbHost;port=$dbPort,$dbUser,$dbPassword"; + + if (defined($dbHost) && defined($dbPort) && defined($dbName) && defined($dbUser) && defined($dbPassword)) { + + $hash->{DBH} = DBI->connect("DBI:mysql:database=$dbName;host=$dbHost;port=$dbPort",$dbUser,$dbPassword); + + if (!defined($hash->{DBH})) { + Log3 $name, 1, "ERROR connecting to database: ".DBI->errstr; + } + else { + Log3 $name, 3, "SmartMeterP1:Connected to database DBI:mysql:database=$dbName;host=$dbHost;port=$dbPort;user=$dbUser"; + $hash->{dbInsertSQL} = $hash->{DBH}->prepare('INSERT INTO smartmeter (`date`,obis_ref,value,unit) VALUES (?,?,?,?)') + } + } + } + +} + +sub +SmartMeterP1_Write2DB($$$$$) +{ + my ($hash,$name,$obis_ref,$date,$valueStr) = @_; + + SmartMeterP1_Connect2DB($hash,$name,AttrVal($name,"write2db",0)) if (!defined($hash->{DBH})); + if (!defined($hash->{DBH})) { + Log3 $name, 4, "SmartMeterP1: Error reconnecting to database."; + return; + } + + my $dateValue = myStr2Date($date); + my $interval = AttrVal($name,"dbUpdateInterval", 5); + if (defined($hash->{$obis_ref})) { + return if ($dateValue < ($hash->{$obis_ref} + ($interval*60))); + } + + $hash->{$obis_ref} = $dateValue; + + my $value; + my $unit; + if ( AttrVal($name,"removeUnitSeparator", "false") eq "true" ) { + ($value,$unit) = split(/ /, $valueStr, 2); + } + else { + ($value,$unit) = split(/\*/, $valueStr, 2); + } + + $hash->{dbInsertSQL}->execute($date,$obis_ref,$value,$unit); + if (DBI->err) { + SmartMeterP1_Connect2DB($hash,$name,AttrVal($name,"write2db",0)); + if (defined($hash->{DBH})) { + $hash->{dbInsertSQL}->execute($date,$obis_ref,$value,$unit); + } + else { + Log3 $name, 1, "SmartMeterP1: Error reconnecting to database."; + return; + } + } + $hash->{dbInsertSQL}->finish; +} + +sub +RemoveLeadingZero($$) +{ + my ($name, $value) = @_; + + if ( AttrVal($name,"removeLeadingZero", "false") eq "true" ) { + $value =~ s/^(0*)?([0-9]+\..*)/$2/; + } + + return $value; +} + +sub +RemoveUnitSeparator($$) +{ + my ($name, $value) = @_; + + if ( AttrVal($name,"removeUnitSeparator", "false") eq "true" ) { + $value =~ s/(.*)?\*(.*)/$1 $2/; + } + + $value = RemoveLeadingZero($name,$value); + + return $value; +} + +sub +SmartMeterP1_ParseTelegramLine($$$@) +{ + my ($hash,$name,$obis_ref,@attributes) = @_; + + Log3 $name, 4, " Telegram obis: $obis_ref"; + Log3 $name, 4, " Telegram attr: ".Dumper(@attributes); + + if ($obis_ref eq "0-0:1.0.0") { + #Date-time stamp of the P1 message + # YYMMDDhhmmssW + $hash->{TelegramTime} = ConvertTelegramTime($attributes[0]); + readingsSingleUpdate($hash,"TelegramTime",$hash->{TelegramTime},1); + } + elsif ($obis_ref eq "1-0:1.8.1") { + # Meter reading electricity delivered to client low Tariff in 0,001 kWh + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ElectricityDeliveredLowTariff", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ElectricityDeliveredLowTariff",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "1-0:1.8.2") { + # Meter reading electricity delivered to client normal Tariff in 0,001 kWh + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ElectricityDeliveredNormalTariff", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ElectricityDeliveredNormalTariff",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "1-0:2.8.1") { + # Meter reading electricity delivered by client low Tariff in 0,001 kWh + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ElectricityProducedLowTariff", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ElectricityProducedLowTariff",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "1-0:2.8.2") { + # Meter reading electricity delivered by client normal Tariff in 0,001 kWh + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ElectricityProducedNormalTariff", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ElectricityProducedNormalTariff",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "1-0:1.7.0") { + # Actual electricity power delivered (+P) in 1 Watt resolution + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ElectricityPowerDelivered", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ElectricityPowerDelivered",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "1-0:2.7.0") { + # Actual electricity power received (-P) in 1 Watt resolution + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ElectricityPowerProduced", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ElectricityPowerProduced",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "0-0:17.0.0") { + # The actual threshold Electricity in kW + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ElectricityThreshold", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ElectricityThreshold",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "0-0:96.14.0") { + # Tariff indicator electricity. + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"TariffIndicatorElectricity", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"TariffIndicatorElectricity",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "0-0:96.3.10") { + # Switch position electricity + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"SwitchPositionElectricity", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"SwitchPositionElectricity",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } + elsif ($obis_ref eq "0-1:24.2.1") { + # Last hourly value gas delivered to client in m3 + $hash->{".updateTimestamp"} = ConvertTelegramTime($attributes[0]); + my $tmp = ReadingsVal($name,"GasDeliveredTime", "-"); + if (($tmp eq "-") || ($tmp ne ConvertTelegramTime($attributes[0]))) { + readingsSingleUpdate($hash,"GasDeliveredTime",ConvertTelegramTime($attributes[0]),1); + $attributes[1] = RemoveUnitSeparator($name, $attributes[1]); + readingsSingleUpdate($hash,"GasDelivered",$attributes[1],1); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,ConvertTelegramTime($attributes[0]),$attributes[1]); + } + } + elsif ($obis_ref eq "0-1:24.3.0") { + $hash->{Telegram}->{Gas}->{used} = 1; + $hash->{Telegram}->{Gas}->{time} = ConvertTelegramTime($attributes[0]); + $hash->{Telegram}->{Gas}->{obis_ref} = $attributes[4]; + $hash->{Telegram}->{Gas}->{unit} = $attributes[5]; + } + elsif ($obis_ref eq "") { + if (($hash->{Telegram}->{Gas}->{used} == 1) && ($hash->{Telegram}->{Gas}->{obis_ref} eq "0-1:24.2.1" )) { + # Last hourly value gas delivered to client in m3 + $hash->{".updateTimestamp"} = $hash->{Telegram}->{Gas}->{time}; + my $tmp = ReadingsVal($name,"GasDeliveredTime", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + if (($tmp eq "-") || ($tmp ne $hash->{Telegram}->{Gas}->{time})) { + readingsSingleUpdate($hash,"GasDeliveredTime",$hash->{Telegram}->{Gas}->{time},1); + if ( AttrVal($name,"removeUnitSeparator", "false") eq "true" ) { + readingsSingleUpdate($hash,"GasDelivered",$attributes[0]." ".$hash->{Telegram}->{Gas}->{unit},1); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{Telegram}->{Gas}->{time},$attributes[0]." ".$hash->{Telegram}->{Gas}->{unit}); + } + else { + readingsSingleUpdate($hash,"GasDelivered",$attributes[0]."*".$hash->{Telegram}->{Gas}->{unit},1); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{Telegram}->{Gas}->{time},$attributes[0]."*".$hash->{Telegram}->{Gas}->{unit}); + } + } + $hash->{Telegram}->{Gas}->{used} = 0; + } + } + elsif ($obis_ref eq "0-1:24.4.0") { + # Valve position Gas + $hash->{".updateTimestamp"} = $hash->{TelegramTime}; + my $tmp = ReadingsVal($name,"ValvePositionGas", "-"); + $attributes[0] = RemoveUnitSeparator($name, $attributes[0]); + readingsSingleUpdate($hash,"ValvePositionGas",$attributes[0],1) if (($tmp eq "-") || ($tmp ne $attributes[0])); + SmartMeterP1_Write2DB($hash,$name,$obis_ref,$hash->{TelegramTime},$attributes[0]) if (($tmp eq "-") || ($tmp ne $attributes[0])); + } +} + +sub +SmartMeterP1_Parse($$$$) +{ + my ($hash, $iohash, $name, $rmsg) = @_; + + Log3 $name, 5, "SmartMeterP1/Telegramline: $rmsg"; + + if ($rmsg =~ m/^\/(.*)/) { + $hash->{TelegramStart} = 1; + $hash->{Telegram} = {}; + $hash->{Telegram}->{Gas} = {}; + $hash->{Telegram}->{Gas}->{used} = 0; + Log3 $name, 4, " Telegram start: $1"; + } + else { + if ($hash->{TelegramStart} == 1) { + if ($rmsg =~ m/^\!(....)/) { + Log3 $name, 4, " Telegram end: $1"; + } + elsif ($rmsg =~ m/^\!/) { + Log3 $name, 4, " Telegram end."; + } + else { + Log3 $name, 4, " Telegram line: $rmsg"; + my $obis_ref; + my @attributes; + ($obis_ref,@attributes) = split('\(', $rmsg); + + my $count = 0; + while ($count < @attributes) { + $attributes[$count] =~ s/\)//; + $count++; + } + + SmartMeterP1_ParseTelegramLine($hash,$name,$obis_ref,@attributes); + } + } + } + +} + + +##################################### +sub +SmartMeterP1_Ready($) +{ + my ($hash) = @_; + + return DevIo_OpenDev($hash, 1, "SmartMeterP1_DoInit") + if($hash->{STATE} eq "disconnected"); + + # This is relevant for windows/USB only + my $po = $hash->{USBDev}; + my ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags); + if($po) { + ($BlockingFlags, $InBytes, $OutBytes, $ErrorFlags) = $po->status; + } + return ($InBytes && $InBytes>0); +} + +sub +SmartMeterP1_Attr(@) +{ + my ($cmd,$name,$aName,$aVal) = @_; + + my $hash = $defs{$name}; + + Log3 $name, 3, "SmartMeterP1:Updating attribute '$aName' to '$aVal'"; + + if (($aName eq "write2db") || ($aName eq "dbHost") || ($aName eq "dbPort") || ($aName eq "dbName") || ($aName eq "dbUser") || ($aName eq "dbPassword")) { + + # Something in the db attributes changed reconnect. + if (defined($hash->{DBH})) { + $hash->{DBH}->disconnect; + undef $hash->{DBH}; + } + +# if ($aName eq "write2db") { +# SmartMeterP1_Connect2DB($hash,$name,$aVal); +# } +# else { +# SmartMeterP1_Connect2DB($hash,$name,AttrVal($name,"write2db",0)); +# } + } + return undef; +} + + +1; + +=pod +=item device +=item summary Read data from your Electricity and Gas smart meter +=begin html + + +

SmartMeterP1

+ + +=end html + +=cut