# # kaihs@FHEM_Forum (forum.fhem.de) # # $Id$ # # package main; use strict; use warnings; use SetExtensions; use WMBus; sub WMBUS_Parse($$); sub WMBUS_SetReadings($$$); sub WMBUS_SetRSSI($$$); sub WMBUS_RSSIAsRaw($); sub WMBUS_Initialize($) { my ($hash) = @_; $hash->{Match} = "^b.*"; #$hash->{SetFn} = "WMBUS_Set"; #$hash->{GetFn} = "WMBUS_Get"; $hash->{DefFn} = "WMBUS_Define"; $hash->{UndefFn} = "WMBUS_Undef"; #$hash->{FingerprintFn} = "WMBUS_Fingerprint"; $hash->{ParseFn} = "WMBUS_Parse"; $hash->{AttrFn} = "WMBUS_Attr"; $hash->{AttrList} = "IODev". " AESkey". " ignore:0,1". " rawmsg_as_reading:0,1". " $readingFnAttributes"; } sub WMBUS_HandleEncoding($$) { my ($mb, $msg) = @_; my $encoding = "CUL"; my $rssi; ($msg, $rssi) = split(/::/,$msg); if (substr($msg,1,3) eq "AMB") { # Amber Wireless AMB8425-M encoding, does not include CRC16 $encoding = "AMB"; $mb->setCRCsize(0); # message length (first byte) contains 1 byte for rssi, # remove it my $msglen = sprintf("%1x", hex(substr($msg,4,1)) - 1); $msg = "b" . $msglen . substr($msg,5); } else { $msg .= WMBUS_RSSIAsRaw($rssi); } return ($msg, $rssi, $encoding); } sub WMBUS_Define($$) { my ($hash, $def) = @_; my @a = split("[ \t][ \t]*", $def); my $mb; my $rssi; if(@a != 6 && @a != 3) { my $msg = "wrong syntax: define WMBUS [ []]|b"; Log3 undef, 2, $msg; return $msg; } my $name = $a[0]; if (@a == 3) { # unparsed message my $msg = $a[2]; $mb = new WMBus; ($msg, $rssi, $hash->{MessageEncoding}) = WMBUS_HandleEncoding($mb, $msg); my $minSize = ($mb->getCRCsize() + WMBus::TL_BLOCK_SIZE) * 2; my $reMinSize = qr/b[a-zA-Z0-9]{${minSize},}/; return "a WMBus message must be a least $minSize bytes long, $msg" if $msg !~ m/${reMinSize}/; if ($mb->parseLinkLayer(pack('H*',substr($msg,1)))) { $hash->{Manufacturer} = $mb->{manufacturer}; $hash->{IdentNumber} = $mb->{afield_id}; $hash->{Version} = $mb->{afield_ver}; $hash->{DeviceType} = $mb->{afield_type}; if ($mb->{errormsg}) { $hash->{Error} = $mb->{errormsg}; } else { delete $hash->{Error}; } WMBUS_SetRSSI($hash, $mb, $rssi); } else { return "failed to parse msg: $mb->{errormsg}"; } } else { my $encoding = "CUL"; # manual specification if ($a[2] !~ m/[A-Z]{3}/) { return "$a[2] is not a valid WMBUS manufacturer id"; } if ($a[3] !~ m/[0-9]{1,8}/) { return "$a[3] is not a valid WMBUS serial number"; } if ($a[4] !~ m/[0-9]{1,2}/) { return "$a[4] is not a valid WMBUS version"; } if ($a[5] !~ m/[0-9]{1,2}/) { return "$a[5] is not a valid WMBUS type"; } if (defined($a[6])) { $encoding = $a[6]; } if ($encoding ne "CUL" && $encoding ne "AMB") { return "$a[6] isn't a supported encoding, use either CUL or AMB"; } $hash->{Manufacturer} = $a[2]; $hash->{IdentNumber} = sprintf("%08d",$a[3]); $hash->{Version} = $a[4]; $hash->{DeviceType} = $a[5]; $hash->{MessageEncoding} = $encoding; } my $addr = join("_", $hash->{Manufacturer},$hash->{IdentNumber},$hash->{Version},$hash->{DeviceType}) ; return "WMBUS device $addr already used for $modules{WMBUS}{defptr}{$addr}->{NAME}." if( $modules{WMBUS}{defptr}{$addr} && $modules{WMBUS}{defptr}{$addr}->{NAME} ne $name ); $hash->{addr} = $addr; $modules{WMBUS}{defptr}{$addr} = $hash; AssignIoPort($hash); if(defined($hash->{IODev}->{NAME})) { Log3 $name, 3, "$name: I/O device is " . $hash->{IODev}->{NAME}; } else { Log3 $name, 1, "$name: no I/O device"; } $hash->{DEF} = join(" ", $hash->{Manufacturer},$hash->{IdentNumber},$hash->{Version},$hash->{DeviceType}); $hash->{DeviceMedium} = WMBus::->type2string($hash->{DeviceType}); if (defined($mb)) { if ($mb->parseApplicationLayer()) { if ($mb->{cifield} == WMBus::CI_RESP_12) { $hash->{Meter_Id} = $mb->{meter_id}; $hash->{Meter_Manufacturer} = $mb->{meter_manufacturer}; $hash->{Meter_Version} = $mb->{meter_vers}; $hash->{Meter_Dev} = $mb->{meter_devtypestring}; $hash->{Access_No} = $mb->{access_no}; $hash->{Status} = $mb->{status}; } WMBUS_SetReadings($hash, $name, $mb); } else { $hash->{Error} = $mb->{errormsg}; } } return undef; } ##################################### sub WMBUS_Undef($$) { my ($hash, $arg) = @_; my $name = $hash->{NAME}; my $addr = $hash->{addr}; delete( $modules{WMBUS}{defptr}{$addr} ); return undef; } ##################################### sub WMBUS_Get($@) { my ($hash, $name, $cmd, @args) = @_; return "\"get $name\" needs at least one parameter" if(@_ < 3); my $list = ""; return "Unknown argument $cmd, choose one of $list"; } sub WMBUS_Fingerprint($$) { my ($name, $msg) = @_; return ( "", $msg ); } sub WMBUS_Parse($$) { my ($hash, $rawMsg) = @_; my $name = $hash->{NAME}; my $addr; my $rhash; my $rssi; my $msg; # $hash is the hash of the IODev! if( $rawMsg =~ m/^b/ ) { # WMBus message received Log3 $name, 5, "WMBUS raw msg " . $rawMsg; my $mb = new WMBus; ($msg, $rssi, $hash->{MessageEncoding}) = WMBUS_HandleEncoding($mb, $rawMsg); if ($mb->parseLinkLayer(pack('H*',substr($msg,1)))) { $addr = join("_", $mb->{manufacturer}, $mb->{afield_id}, $mb->{afield_ver}, $mb->{afield_type}); $rhash = $modules{WMBUS}{defptr}{$addr}; if( !$rhash ) { Log3 $name, 3, "WMBUS Unknown device $rawMsg, please define it"; return "UNDEFINED WMBUS_$addr WMBUS $rawMsg"; } my $rname = $rhash->{NAME}; return "" if(IsIgnored($rname)); WMBUS_SetRSSI($rhash, $mb, $rssi); my $aeskey; if ($aeskey = AttrVal($rname, 'AESkey', undef)) { $mb->{aeskey} = pack("H*",$aeskey); } else { $mb->{aeskey} = undef; } if ($mb->parseApplicationLayer()) { return WMBUS_SetReadings($rhash, $rname, $mb); } else { Log3 $rname, 2, "WMBUS $rname Error during ApplicationLayer parse:" . $mb->{errormsg}; readingsSingleUpdate($rhash, "state", $mb->{errormsg}, 1); return $rname; } } else { # error Log3 $name, 2, "WMBUS Error during LinkLayer parse:" . $mb->{errormsg}; return undef; } } else { DoTrigger($name, "UNKNOWNCODE $rawMsg"); Log3 $name, 3, "$name: Unknown code $rawMsg, help me!"; return undef; } } # if the culfw doesn't send the RSSI value (because it is an old version that doesn't implement this) but 00_CUL.pm already expects it # one byte is missing from the data which leads to CRC errors # To avoid this calculate the raw data byte from the RSSI and append it to the data. # If it is a valid RSSI it will be ignored by the WMBus parser (the data contains the length of the data itself # and only that much is parsed). sub WMBUS_RSSIAsRaw($) { my $rssi = shift; if (defined $rssi) { if ($rssi < -74) { $b = ($rssi+74)*2+256; } else { $b = ($rssi+74)*2; } return sprintf("%02X", $b); } else { return ""; } } sub WMBUS_SetRSSI($$$) { my ($hash, $mb, $rssi) = @_; if (defined $mb->{remainingData} && length($mb->{remainingData}) >= 2) { # if there are trailing bytes after the WMBUS message it is the LQI and the RSSI readingsBeginUpdate($hash); my ($lqi, $rssi) = unpack("CC", $mb->{remainingData}); $rssi = ($rssi>=128 ? (($rssi-256)/2-74) : ($rssi/2-74)); readingsBulkUpdate($hash, "RSSI", $rssi); readingsBulkUpdate($hash, "LQI", unpack("C", $mb->{remainingData})); readingsEndUpdate($hash,1); } } sub WMBUS_SetReadings($$$) { my ($hash, $name, $mb) = @_; my @list; push(@list, $name); readingsBeginUpdate($hash); if ($mb->{decrypted}) { my $dataBlocks = $mb->{datablocks}; my $dataBlock; for $dataBlock ( @$dataBlocks ) { readingsBulkUpdate($hash, "$dataBlock->{number}_storage_no", $dataBlock->{storageNo}); readingsBulkUpdate($hash, "$dataBlock->{number}_type", $dataBlock->{type}); readingsBulkUpdate($hash, "$dataBlock->{number}_value", $dataBlock->{value}); readingsBulkUpdate($hash, "$dataBlock->{number}_unit", $dataBlock->{unit}); readingsBulkUpdate($hash, "$dataBlock->{number}_value_type", $dataBlock->{functionFieldText}); if (defined($dataBlock->{extension})) { readingsBulkUpdate($hash, "$dataBlock->{number}_extension", $dataBlock->{extension}); } if ($dataBlock->{errormsg}) { readingsBulkUpdate($hash, "$dataBlock->{number}_errormsg", $dataBlock->{errormsg}); } } readingsBulkUpdate($hash, "battery", $mb->{status} & 4 ? "low" : "ok"); WMBUS_SetDeviceSpecificReadings($hash, $name, $mb); } readingsBulkUpdate($hash, "is_encrypted", $mb->{isEncrypted}); readingsBulkUpdate($hash, "decryption_ok", $mb->{decrypted}); if ($mb->{decrypted}) { readingsBulkUpdate($hash, "state", $mb->{statusstring}); } else { readingsBulkUpdate($hash, "state", 'decryption failed'); } if (AttrVal($name, "rawmsg_as_reading", 0)) { readingsBulkUpdate($hash, "rawmsg", unpack("H*",$mb->{msg})); } readingsEndUpdate($hash,1); return @list; } sub WMBUS_SetDeviceSpecificReadings($$$) { my ($hash, $name, $mb) = @_; if ($mb->{manufacturer} eq 'FFD') { # Fast Forward AG if ($mb->{afield_ver} == 1) { #EnergyCam if ($mb->{afield_type} == 2) { # electricity readingsBulkUpdate($hash, "energy", ReadingsVal($name, "1_value", 0) / 1000); readingsBulkUpdate($hash, "unit", "kWh"); } elsif ($mb->{afield_type} == 3 || $mb->{afield_type} == 7) { # gas/water readingsBulkUpdate($hash, "volume", ReadingsVal($name, "1_value", 0)); readingsBulkUpdate($hash, "unit", "m³"); } } } elsif ($mb->{afield_type} == 3 || $mb->{afield_type} == 7) { # general gas/water meter my $dataBlock; my $dataBlocks = $mb->{datablocks}; for $dataBlock ( @$dataBlocks ) { # search for VIF_VOLUME if ($dataBlock->{type} eq 'VIF_VOLUME' && $dataBlock->{functionFieldText} eq "Instantaneous value") { readingsBulkUpdate($hash, "volume", $dataBlock->{value}); readingsBulkUpdate($hash, "unit", $dataBlock->{unit}); } } } } ##################################### sub WMBUS_Set($@) { my ($hash, @a) = @_; my $name = shift @a; my $cmd = shift @a; my $arg = join(" ", @a); my $list = "resetAccumulatedPower"; return $list if( $cmd eq '?' || $cmd eq ''); if($cmd eq "resetAccumulatedPower") { CommandAttr(undef, "$name accumulatedPowerOffset " . $hash->{READINGS}{accumulatedPowerMeasured}{VAL}); } else { return "Unknown argument $cmd, choose one of ".$list; } return undef; } sub WMBUS_Attr(@) { my ($cmd, $name, $attrName, $attrVal) = @_; my $hash = $defs{$name}; my $msg = ''; if ($attrName eq 'AESkey') { if ($attrVal =~ /^[0-9A-Fa-f]{32}$/) { $hash->{wmbus}->{aeskey} = $attrVal; } else { $msg = "AESkey must be a 32 digit hexadecimal value"; } } return ($msg) ? $msg : undef; } 1; =pod =item device =item summary Reception of Wireless M-Bus messages from e.g. electicity meters =item summary_DE Empfang von Wireless M-Bus Nachrichten z. B. von Stromzählern =begin html

WMBUS - Wireless M-Bus

    This module supports Wireless M-Bus meters for e.g. water, heat, gas or electricity. Wireless M-Bus is a standard protocol supported by various manufacturers. It uses the 868 MHz band for radio transmissions. Therefore you need a device which can receive Wireless M-Bus messages, e.g. a CUL with culfw >= 1.59 or an AMBER Wireless AMB8465M.
    WMBus uses two different radio protocols, T-Mode and S-Mode. The receiver must be configured to use the same protocol as the sender. In case of a CUL this can be done by setting rfmode to WMBus_T or WMBus_S respectively.
    WMBus devices send data periodically depending on their configuration. It can take days between individual messages or they might be sent every minute.
    WMBus messages can be optionally encrypted. In that case the matching AESkey must be specified with attr AESkey. Otherwise the decryption will fail and no relevant data will be available.

    Prerequisites
    This module requires the perl modules Crypt::CBC, Digest::CRC and Crypt::OpenSSL::AES (AES only if encrypted messages should be processed).
    On a debian based system these can be installed with
    sudo apt-get install libcrypt-cbc-perl libdigest-crc-perl libssl-dev
    sudo cpan -i Crypt::OpenSSL::AES


    Define
      define <name> WMBUS [<manufacturer id> <identification number> <version> <type> [<MessageEncoding>]]|<bHexCode>

      Normally a WMBus device isn't defined manually but automatically through the autocreate mechanism upon the first reception of a message.
      For a manual definition there are two ways.
      • By specifying a raw WMBus message as received by a CUL. Such a message starts with a lower case 'b' and contains at least 24 hexadecimal digits. The WMBUS module extracts all relevant information from such a message.
      • Explictly specify the information that uniquely identifies a WMBus device.
        The manufacturer code, which is is a three letter shortcut of the manufacturer name. See dlms.com for a list of registered ids.
        The identification number is the serial no of the meter.
        version is the version code of the meter
        type is the type of the meter, e.g. water or electricity encoded as a number.
        MessageEncoding is either CUL or AMB, depending on which kind of IODev is used.


    Set
      N/A

    Get
      N/A

    Attributes
    • IODev
      Set the IO or physical device which should be used for receiving signals for this "logical" device. An example for the physical device is a CUL.

    • AESKey
      A 16 byte AES-Key in hexadecimal digits. Used to decrypt messages from meters which have encryption enabled.

    • ignore

    • rawmsg_as_reading
      If set to 1, received raw messages will be stored in the reading rawmsg. This can be used to log raw messages to help with debugging.

    Readings
      Meters can send a lot of different information depending on their type. An electricity meter will send other data than a water meter. The information also depends on the manufacturer of the meter. See the WMBus specification on oms-group.org for details.

      The readings are generated in blocks starting with block 1. A meter can send several data blocks. Each block has at least a type, a value and a unit, e.g. for an electricity meter it might look like
        1_type VIF_ENERGY_WATT
        1_unit Wh
        1_value 2948787

      There is also a fixed set of readings.
      • is_encrypted is 1 if the received message is encrypted.
      • decryption_ok is 1 if a message has either been successfully decrypted or if it is unencrypted.
      • state contains the state of the meter and may contain error message like battery low. Normally it contains 'no error'.
      • battery contains ok or low.
      For some well known devices specific readings like the energy consumption in kWh created.
=end html =begin html_DE

WMBUS - Wireless M-Bus

    Dieses Modul unterstützt Zähler mit Wireless M-Bus, z. B. für Wasser, Gas oder Elektrizität. Wireless M-Bus ist ein standardisiertes Protokoll das von unterschiedlichen Herstellern unterstützt wird. Es verwendet das 868 MHz Band für Radioübertragungen. Daher wird ein Gerät benötigt das die Wireless M-Bus Nachrichten empfangen kann, z. B. ein CUL mit culfw >= 1.59 oder ein AMBER Wireless AMB8465-M.
    WMBus verwendet zwei unterschiedliche Radioprotokolle, T-Mode und S-Mode. Der Empfänger muss daher so konfiguriert werden, dass er das selbe Protokoll verwendet wie der Sender. Im Falle eines CUL kann das erreicht werden, in dem das Attribut rfmode auf WMBus_T bzw. WMBus_S gesetzt wird.
    WMBus Geräte senden Daten periodisch abhängig von ihrer Konfiguration. Es können u. U. Tage zwischen einzelnen Nachrichten vergehen oder sie können im Minutentakt gesendet werden.
    WMBus Nachrichten können optional verschlüsselt werden. Bei verschlüsselten Nachrichten muss der passende Schlüssel mit dem Attribut AESkey angegeben werden. Andernfalls wird die Entschlüsselung fehlschlagen und es können keine relevanten Daten ausgelesen werden.

    Voraussetzungen
    Dieses Modul benötigt die perl Module Crypt::CBC, Digest::CRC and Crypt::OpenSSL::AES (AES wird nur benötigt wenn verschlüsselte Nachrichten verarbeitet werden sollen).
    Bei einem Debian basierten System können diese so installiert werden
    sudo apt-get install libcrypt-cbc-perl libdigest-crc-perl libssl-dev
    sudo cpan -i Crypt::OpenSSL::AES


    Define
      define <name> WMBUS [<manufacturer id> <identification number> <version> <type> [<:MessageEncoding>]]|<bHexCode>

      Normalerweise wird ein WMBus Device nicht manuell angelegt. Dies geschieht automatisch bem Empfang der ersten Nachrichten eines Gerätes über den fhem autocreate Mechanismus.
      Für eine manuelle Definition gibt es zwei Wege.
      • Durch Verwendung einer WMBus Rohnachricht wie sie vom IODev empfangen wurde. So eine Nachricht beginnt mit einem kleinen 'b' und enthält mindestens 24 hexadezimale Zeichen. Das WMBUS Modul extrahiert daraus alle benötigten Informationen.
      • Durch explizite Angabe der Informationen die ein WMBus Gerät eindeutig identfizieren.
        Der Hersteller Code, besteht aus drei Buchstaben als Abkürzung des Herstellernamens. Eine Liste der Abkürzungen findet sich unter dlms.com
        Die Idenitfikationsnummer ist die Seriennummer des Zählers.
        Version ist ein Versionscode des Zählers.
        Typ ist die Art des Zählers, z. B. Wasser oder Elektrizität, kodiert als Zahl.
        MessageEncoding ist entweder CUL oder AMB, je nachdem welche Art von IODev verwendet wird


    Set
      N/A

    Get
      N/A

    Attributes
    • IODev
      Setzt den IO oder physisches Gerät welches für den Empfang der Signale für dieses 'logische' Gerät verwendet werden soll. Ein Beispiel für ein solches Gerät ist ein CUL.

    • AESKey
      Ein 16 Bytes langer AES-Schlüssel in hexadezimaler Schreibweise. Wird verwendet um Nachrichten von Zählern zu entschlüsseln bei denen die Verschlüsselung aktiviert ist.

    • ignore

    • rawmsg_as_reading
      Wenn auf 1 gesetzt so werden empfangene Nachrichten im Reading rawmsg gespeichert. Das kann verwendet werden um Rohnachrichten zu loggen und beim Debugging zu helfen.

    Readings
      Zähler können sehr viele unterschiedliche Informationen senden, abhängig von ihrem Typ. Ein Elektrizitätszähler wird andere Daten senden als ein Wasserzähler. Die Information hängt auch vom Hersteller des Zählers ab. Für weitere Informationen siehe die WMBus Spezifikation unter oms-group.org.

      Die Readings werden als Block dargestellt, beginnend mit Block 1. Ein Zähler kann mehrere Blöcke senden. Jeder Block enthält zumindest einen Typ, einen Wert und eine Einheit. Für einen Elektrizitätszähler könnte das z. B. so aussehen
        1_type VIF_ENERGY_WATT
        1_unit Wh
        1_value 2948787

      Es gibt auch eine Anzahl von festen Readings.
      • is_encrypted ist 1 wenn die empfangene Nachricht verschlüsselt ist.
      • decryption_ok ist 1 wenn die Nachricht entweder erfolgreich entschlüsselt wurde oder gar nicht verschlüsselt war.
      • state enthält den Status des Zählers und kann Fehlermeldungen wie 'battery low' enthalten. Normalerweise ist der Wert 'no error'.
      • battery enthält ok oder low.
      Für einige bekannte Gerätetypen werden zusätzliche Readings wie der Energieverbrauch in kWh erzeugt.
=end html_DE =cut