App-KamstrupKemSplit
view release on metacpan or search on metacpan
lib/App/KamstrupKemSplit.pm view on Meta::CPAN
This script takes as input a delivery file from Kamstrup (encrypted, compressed KEM file), unpacks it and splits the file into different
decoded XML files that can be further processed.
Minimal input to the script are the encryption key and the input file. If no further configuration file is passed then all information
in the input file is written to the output file.
A configuration file consists of a CSV file with `;` as delimeter and the following columns:
C<kamstrup_ordernr;kamstrup_serial_number_start;kamstrup_serial_number_end;number_of_devices;internal_batch_number>
The output file name will be [kamstrup_ordernr]_[internal_batch_number].
Following functions are available in this package:
=over
=item unzip_kem
Extracts the KEM file from the archive file delivered by Kamstrup.
Do not forget to delete the file after processing.
Takes the archive file name as input.
Returns the filename.
=cut
sub unzip_kem {
my $input_file = shift();
# Unzip the file
INFO "Opening $ input_file archive file...";
my $zip = Archive::Zip->new();
my $status = $zip->read($input_file);
LOGDIE "Read of $input_file failed\n" if $status != AZ_OK;
# There should be only a single kem in the zipfile
my @kems = $zip->membersMatching('.*\.kem');
LOGDIE "Please examine the zipfile, it does not contain a single kem file"
if ( scalar(@kems) != 1 );
my $filename = $kems[0]->{'fileName'};
DEBUG "Kem filename in archive : " . $filename . " -> unzip";
$status = $zip->extractMemberWithoutPaths($filename);
LOGDIE "Extracting $filename from archive failed\n" if $status != AZ_OK;
return $filename;
}
=item decode_kem
Decode an encrypted KEM file, requires the input filename and the encryption key.
Returns the decrypted XML contents of the KEM file as string.
=cut
sub decode_kem {
my $input_file = shift();
my $key = shift();
my $kemformat = shift() // 2;
my $kem_xml = XMLin($input_file);
DEBUG "Decoding encrypted section from XML with key '$key'";
my $data = decode_base64( $kem_xml->{CipherData}->{CipherValue} );
my $fullkey = $key . ( "\0" x ( 16 - length($key) ) );
my $cipher = Crypt::Rijndael->new( $fullkey, Crypt::Rijndael::MODE_CBC() );
my $plain_xml = $cipher->decrypt($data);
my $fix_head = $kemformat == 2 ? "<Devices schem" : "<MetersInOrder";
# Fix the XML
substr( $plain_xml, 0, 14 ) = $fix_head;
chomp($plain_xml);
# Remove trailing characters after last closing bracket in the XML
if ( $plain_xml =~ /(<.+>)/ ) {
$plain_xml = $1;
}
return $plain_xml;
}
=item split_order
Extracts the contents of a specific order from the combined Kamstrup KEM file.
Takes as input the parsed content of the KEM file (meter details),
the lowest meter number in the order,
and the highest meter number in the order.
Returns all meter information of the devices that match the filter criteria.
=cut
sub split_order {
my $meters = shift();
my $nr_min = shift();
my $nr_max = shift();
my $response;
foreach my $meter ( @{ $meters->{'Meter'} } ) {
# If we want to dump all devices, $nr_max will be -1, so if this is not the case do the check and otherwise just dump all
if ($nr_max == -1) {
$response->{$meter} = $meter;
} elsif ( $meter->{'MeterNo'} >= $nr_min && $meter->{'MeterNo'} <= $nr_max ) {
$response->{$meter} = $meter;
}
}
return $response;
}
=item read_config
Read a CSV configuration file containing the various sub orders.
CSV needs to be separated with ';' and needs to contain the headers 'kamstrup_ordernr', 'kamstrup_serial_number_start',
'kamstrup_serial_number_end', 'number_of_devices' and 'internal_batch_number'.
=cut
sub read_config {
my $csv_file = shift();
# Init the CSV reader
my $csv = Text::CSV->new(
{
binary => 1,
auto_diag => 1,
sep_char => ';',
allow_loose_quotes => 1,
( run in 0.978 second using v1.01-cache-2.11-cpan-e1769b4cff6 )