Erweiterung SmartMeter P1 verschlüsselt

Begonnen von sidney, 08 Oktober 2023, 15:08:35

Vorheriges Thema - Nächstes Thema

sidney

Das im Modul 00_SmartMeterP1.pm beschriebene smarte Stromzähler-Modul kommt derzeit in Luxemburg in allen Haushalten zum Einsatz, allerdings mit dem Unterschied, die Daten sind AES verschlüsselt. Ich habe den Original-Code so abgeändert, dass die verschlüsselten Zähler damit funktionieren. Anbei die geänderten Stellen im Code mit Kommentar. Wenn jemand Interesse hat, den Code etwas zu säubern und auch rückwertskompatibel zu machen (dass das Modul mit verschlüsselten und unverschlüsselten Zählern funktioniert) dann als offizielles Modul einzustellen, kann er das gerne tun. Ich kann auch dabei gerne unterstützen.

Übrigens den AES-Schlüssel kann man beim Elektrizitätswerk per Email anfragen. Man muss hier lediglich die Zählernummer angeben (oder Foto vom Zähler machen), Name und Adresse in der Anfrage mit angeben.

Zusätzlich benutzte Perl-Funktionen. Die zusätzlichen Funktionen müssen vorher (z.B. über CPAN) installiert werden, falls sie noch nicht sind:
use bytes;
use Crypt::AuthEnc::GCM;

zusätzlicher Parameter (4) im Define um den AES-Schlüssel einzugeben:
  if(@a != 4) {
    my $msg = "wrong syntax: define <name> SmartMeterP1 {none | devicename[\@baudrate]} <crypto key>";

ein paar Zeilen weiter, Schlüssel-Variable definieren:
  my $key = $a[3];
ein paar Zeilen weiter, Schlüssel als interner Wert ablegen:
  $hash->{CryptoKey} = $key;
abändern der Lese-Funktion für das Lesen der verschlüsselten Daten:
Beim Parsen erkennt man das Blockende nicht mehr, weil die Daten jetzt verschlüsselt sind, also muss man die Daten holen wie sie kommen und zuerst entschlüsseln, bevor man das Blockende sehen kann.
sub
SmartMeterP1_Read($)
{
  my $pstring = "db085341476770";
  $pstring =~ s/([a-fA-F0-9][a-fA-F0-9])/chr(hex($1))/eg;

  my ($hash) = @_;

  my $buf = DevIo_SimpleRead($hash);
  return "" if(!defined($buf));
  my $name = $hash->{NAME};

  my $culdata;
  if (defined $hash->{helper}{Partial}) {
    $culdata = $hash->{helper}{Partial};
  }
  else {
    $culdata = '';
  }
  Log3 $name, 5, "SmartMeterP1/RAW: $culdata/$buf";
  $culdata .= $buf;

  while($culdata =~ m/$pstring.*?$pstring/s) {
     $culdata =~ s/\A.*?\Q$pstring/$pstring/s;
     my ($rmsg,$newculdata) = $culdata =~ m/(\A\Q$pstring\E.+?)(\Q$pstring\E.*?\z)/s;
     $culdata = $newculdata;
     $rmsg = SmartMeterP1_Decrypt($hash , $rmsg);
     $hash->{DECODED} = $rmsg;
     if ($rmsg) {
       my @rmsglines = split "\n", $rmsg;
       foreach (@rmsglines) {
         SmartMeterP1_Parse($hash, $hash, $name, $_) if($_);
       }
     }
  }
  $hash->{helper}{Partial} = $culdata;
}

Erweiterung der geparsten Daten. Dies hat nichts mit der Verschlüsselung zu tun, sondern ist lediglich eine Vervollständigung/Erweiterung der ausgelesenen Daten des Zählers gegenüber der ursprünglichen Version :
  elsif ($obis_ref eq "1-0:1.8.0") {
# Meter reading energy delivered to client in 0,001 kWh
$hash->{".updateTimestamp"} = $hash->{TelegramTime};
my $tmp = ReadingsVal($name,"EnergyDelivered", "-");
$attributes[0] = RemoveUnitSeparator($name, $attributes[0]);
readingsSingleUpdate($hash,"EnergyDelivered",$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.0") {
# Meter reading energy produced by client in 0,001 kWh
$hash->{".updateTimestamp"} = $hash->{TelegramTime};
my $tmp = ReadingsVal($name,"EnergyProduced", "-");
$attributes[0] = RemoveUnitSeparator($name, $attributes[0]);
readingsSingleUpdate($hash,"EnergyProduced",$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:3.8.0") {
# Meter reading vertual energy delivered to client in 0,001 kWh
$hash->{".updateTimestamp"} = $hash->{TelegramTime};
my $tmp = ReadingsVal($name,"VirtualEnergyDelivered", "-");
$attributes[0] = RemoveUnitSeparator($name, $attributes[0]);
readingsSingleUpdate($hash,"VirtualEnergyDelivered",$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:4.8.0") {
# Meter reading energy produced by client in 0,001 kWh
$hash->{".updateTimestamp"} = $hash->{TelegramTime};
my $tmp = ReadingsVal($name,"VirtualEnergyProduced", "-");
$attributes[0] = RemoveUnitSeparator($name, $attributes[0]);
readingsSingleUpdate($hash,"VirtualEnergyProduced",$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]));
  }

und der 2te Teil:
elsif ($obis_ref eq "1-0:3.7.0") {
# Virtual electricity power received (+Q) in 1 Watt resolution
$hash->{".updateTimestamp"} = $hash->{TelegramTime};
my $tmp = ReadingsVal($name,"ElectricityVirtualPowerDelivered", "-");
$attributes[0] = RemoveUnitSeparator($name, $attributes[0]);
readingsSingleUpdate($hash,"ElectricityVirtualPowerDelivered",$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:4.7.0") {
# Actual electricity power produced (-Q) in 1 Watt resolution
$hash->{".updateTimestamp"} = $hash->{TelegramTime};
my $tmp = ReadingsVal($name,"ElectricityVirtualPowerProduced", "-");
$attributes[0] = RemoveUnitSeparator($name, $attributes[0]);
readingsSingleUpdate($hash,"ElectricityVirtualPowerProduced",$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]));

und zuguterletzt die Funktion zum Entschlüsseln am Ende:
sub
SmartMeterP1_Decrypt($$)
{
  my ($hash, $encrypted) = @_;

  my ($startbyte, $title_len, $sys_title, $eighty_two, $data_len, $thirty, $frame_cnt, $remainder) = $encrypted =~ m/\A(.)(.)(.{8})(.)(.{2})(.)(.{4})(.+?)\z/s;

  my $data_len_dec = ord(substr($data_len, 0, 1))  * 256 + ord(substr($data_len, 1, 1)) -17;

  my ($data,$expected_tag)  = $remainder =~ m:\A(.{$data_len_dec})(.{12})\z:s;

  my $aad = "3000112233445566778899AABBCCDDEEFF";
  $aad =~ s/([[:xdigit:]]{2})/chr(hex($1))/seg;


   my $key = $hash->{CryptoKey};
  $key =~ s/([[:xdigit:]]{2})/chr(hex($1))/seg;

  my $iv = $sys_title . $frame_cnt;

 
  my $ae = Crypt::AuthEnc::GCM -> new('AES',$key,$iv);

  $ae -> adata_add($aad);

  my $pt = $ae -> decrypt_add($data);

  my $result = $ae -> decrypt_done();

  if ($expected_tag eq substr($result,0,12)){
    return $pt;
  }
  else {
    return '';
  }
}