Maybe someone will be interesting to use them, the code is not perfect but works. Maybe someone (better coder than me) would like to develop this part of automation…
Configuration:
- Electricity Meter: Apator Otus 3
- Protocol: Wireless M-BUS
- Frequency of data transmission: -f 868.95M (configuration parameter for RTL_433)
- Device: any SDR DVB-T
All data from SDR + RTL_433 are published to MQTT and read to any item of OpenHab.
Defined rule calls exec bin if state of item has been changed and run Java program (located in conf/misc/). Java is calling with 1 parameter [encoded wm-bus data] and return JSon with decoded and parsed data from electicity meter. Decoded data are return to Output item in exec bin.
I realize that this is not an elegant way to solve the problem, but it works, and I did not know how else to call the program code in Java and use additional libraries in OH.
The same you can do this to write data to file and read that in this Java program.
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Calendar;
import java.util.List;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class WMBusDecryption {
private static final String[] deviceType = {
"Other", "Oil", "Electricity", "Gas", "Heat", "Steam", "Hot Water", "Water", "H.C.A.", "Reserved",
"Gas Mode 2", "Heat Mode 2", "Hot Water Mode 2", "Water Mode 2", "H.C.A. Mode 2", "Reserved"
};
public static void main(String[] args) {
String vsInputFile = "";
String vsOutputFile = "";
String rawData = "";
if (args.length != 1) {
//System.out.println("Usage: java WMBusDecryption.java <inputFile> <outputFile>");
// throw new Exception("\nUsage: java WMBusDecryption.java <inputFile> <outputFile>");
vsInputFile = "R:\\_OpenHAB\\_OpenHAB\\conf\\misc\\ElectricityMeter-Data.log";
vsOutputFile = "R:\\_OpenHAB\\_OpenHAB\\conf\\misc\\ElectricityMeter-Data-Decoded.log";
rawData = readDataFromFile(vsInputFile);
} else {
//vsInputFile = args[0]; // "R:\_OpenHAB\_OpenHAB\conf\misc\ElectricityMeter-Data.log";
//vsOutputFile = args[1];
String vsTemp = args[0];
if (vsTemp.length() !=446) {
System.out.println("Wrong format of WM-BUS data!");
System.exit(1);
} else {
rawData = vsTemp;
}
}
String vsJson = "{";
String hexKey = "954c70b3875254bb38309a399f365185"; // Wstaw swój klucz hex
//String rawData = "de4401065443925601027a6a00d0052caf880ebaf681b0ef1ce7202850f95669ac9e7c225069b381b2d9a4c6c74805f6ed8bf016462b283c56bd25381d4e6e1ecfbf9c42983cde72c48cf4c7fdf70fdcf79d63deb1da14b69889c9394d7b7c82957dc699f296087697988396eb11f1a38c06e444803aec56d6681ae19c7caf8fb7b481da47ea522df1f3b33b4b0662b1617bec975155c6dc4e91b480d888a20eade6f3b9d2b88f1f156ea8e4acb2340096488d21bbb77be587b1764af776ad39c147ca18ee6dba834c00027880faa637a33fc42a5364e5bee818df64465c97";
rawData = rawData.trim();
//System.out.print("rawData from file after trim():\n" + rawData);
// Konwersja tablicy bajtów na tablicę hex
String[] hexDecryptedArray = new String[0];
byte[] decryptedData = new byte[0];
byte[] rawDataByte = hexToByteArray(rawData);
String L_Field = String.valueOf(rawDataByte[0] & 0xFF); // length
String C_Field = String.valueOf(rawDataByte[1] & 0xFF); // dll-c (from meter SND_NR)
String Manufacturer_ID = getManufacturerShort(new int[]{rawDataByte[2] & 0xFF, rawDataByte[3] & 0xFF});
//System.out.println("\nManufacturer_ID: " + Manufacturer_ID);
String DeviceID = swapBytes(rawData.substring(8, 16)); // Identification Number of the ELECTRICITY device
String Generation = String.valueOf(rawDataByte[8] & 0xFF); // Generation
String Medium = deviceType[rawDataByte[9] & 0xFF]; // Electricity
String CI_Field = String.valueOf(rawDataByte[10] & 0xFF); // Short 4 bytes APL header
String ACC = String.valueOf(rawDataByte[11] & 0xFF); // Access number
String STS = String.valueOf(rawDataByte[12] & 0xFF); // M-Bus state contents
String ConfWord_Size = String.valueOf(rawDataByte[13] & 0xFF); // 208
String ConfWord_EncryptionMode = String.valueOf(rawDataByte[14] & 0xFF); // EncryptionMode: 5 - AES-128-CBC
// do sprawdzenia czy ok
//System.out.println("ConfWord_Size: " + ConfWord_Size);
//System.out.println("ConfWord_EncryptionMode: " + ConfWord_EncryptionMode);
vsJson += "\"Manufacturer_ID\":\"" + Manufacturer_ID + "\"," + "\"DeviceID\":\"" + DeviceID + "\"," + "\"Generation\":" + Generation + "," + "\"Medium\":\"" + Medium + "\",";
//System.out.println("\nvsJson: " + vsJson);
String IV = byteArrayToHex(Arrays.copyOfRange(rawDataByte, 2, 10));
String IVfill = byteArrayToHex(new byte[]{rawDataByte[11]});
for (int i = 1; i < 9; i++) {
IV += IVfill;
}
//System.out.println("IV: " + IV);
//System.out.println("IV length: " + IV.length());
//System.out.println("hexKey: " + hexKey);
//System.out.println("hexKey length: " + hexKey.length());
String encryptedData = getEncryptedData(15, Integer.parseInt(ConfWord_Size), rawDataByte).toUpperCase();
//System.out.println("encrypted data (part) for decryption:\n" + encryptedData);
//System.out.println("encrypted data length: " + encryptedData.length()/2);
try {
decryptedData = decryptAES128CBC(encryptedData, hexKey, IV);
// Wyświetlenie wyniku tablicy hex odszyfrowanej
hexDecryptedArray = bytesArrayToHexArray(decryptedData);
/*System.out.println("decrypted data:\n ");
//check - fajna pętla w Java'ie
for (String hex : hexDecryptedArray) {
System.out.print(hex);
}
*/
String vsCheck = hexDecryptedArray[0] + hexDecryptedArray[1];
//System.out.println("\nCheck decrypt2: " + vsCheck);
if (vsCheck.equals("2F2F")) {
//System.out.println("\nPrawidlowo odszyfrowano :-)");
String vsDateTime = getTimeStamp(Arrays.copyOfRange(hexDecryptedArray, 4, 10));
vsJson += "\"DateTime\":\"" + vsDateTime + "\",";
// total_energy_consumption_kwh
String vsTEC = swapBytes(hexDecryptedArray[18] + hexDecryptedArray[19] + hexDecryptedArray[20] + hexDecryptedArray[21] + hexDecryptedArray[22] + hexDecryptedArray[23]);
float vfTEC = Float.parseFloat(vsTEC) / 1000;
String total_energy_consumption_kwh = String.valueOf(vfTEC);
//System.out.println("\ntotal_energy_consumption_kwh: " + vsTEC + " " + total_energy_consumption_kwh);
vsJson += "\"Total_energy_consumption_kWh\":" + total_energy_consumption_kwh + ",";
// total_energy_production_kwh
String vsTEP = swapBytes(hexDecryptedArray[64] + hexDecryptedArray[65] + hexDecryptedArray[66] + hexDecryptedArray[67] + hexDecryptedArray[68] + hexDecryptedArray[69]);
float vfTEP = Float.parseFloat(vsTEP) / 1000;
String total_energy_production_kwh = String.valueOf(vfTEP);
//System.out.println("\ntotal_energy_production_kwh: " + vsTEP + " " + total_energy_production_kwh);
vsJson += "\"Total_energy_production_kWh\":" + total_energy_production_kwh + ",";
// current_power_consumption_kw
String vsCPC = swapBytes(hexDecryptedArray[113] + hexDecryptedArray[114] + hexDecryptedArray[115]);
float vfCPC = Float.parseFloat(vsCPC) / 1000;
String current_power_consumption_kw = String.valueOf(vfCPC);
//System.out.println("\ncurrent_power_consumption_kw: " + vsCPC + " " + current_power_consumption_kw);
vsJson += "\"Current_power_consumption_kW\":" + current_power_consumption_kw + ",";
// current_power_production_kw
String vsCPP = swapBytes(hexDecryptedArray[119] + hexDecryptedArray[120] + hexDecryptedArray[121]);
float vfCPP = Float.parseFloat(vsCPP) / 1000;
String current_power_production_kw = String.valueOf(vfCPP);
//System.out.println("\ncurrent_power_production_kw: " + vsCPP + " " + current_power_production_kw);
vsJson += "\"Current_power_production_kW\":" + current_power_production_kw + ",";
// Voltage_1
String vsV1 = swapBytes(hexDecryptedArray[140] + hexDecryptedArray[141]);
float vfV1 = Float.parseFloat(vsV1) / 10;
String Voltage_1 = String.valueOf(vfV1);
//System.out.println("\nVoltage_1: " + vsV1 + " " + Voltage_1);
vsJson += "\"Voltage_1\":" + Voltage_1 + ",";
// Voltage_2
String vsV2 = swapBytes(hexDecryptedArray[147] + hexDecryptedArray[148]);
float vfV2 = Float.parseFloat(vsV2) / 10;
String Voltage_2 = String.valueOf(vfV1);
//System.out.println("\nVoltage_2: " + vsV2 + " " + Voltage_2);
vsJson += "\"Voltage_2\":" + Voltage_2 + ",";
// Voltage_3
String vsV3 = swapBytes(hexDecryptedArray[154] + hexDecryptedArray[155]);
float vfV3 = Float.parseFloat(vsV3) / 10;
String Voltage_3 = String.valueOf(vfV3);
//System.out.println("\nVoltage_3: " + vsV3 + " " + Voltage_3);
vsJson += "\"Voltage_3\":" + Voltage_3 + ",";
// Voltage_AVG
float vfVA = ((vfV1 + vfV2 + vfV3)/3);
vfVA = Math.round(vfVA * 10) / 10f;
//System.out.println("\nVoltage_AVG: " + vfVA);
vsJson += "\"Voltage_AVG\":" + vfVA;
vsJson += "}";
System.out.println(vsJson);
if (vsOutputFile.length() != 0) {
writeDataToFile(vsOutputFile, vsJson);
}
} else {
throw new Exception("\nNieprawidlowo odszyfrowano dane :-(");
}
} catch (Exception e) {
//e.printStackTrace();
System.out.println("Wystąpił wyjątek: " + e.getMessage());
}
}
// Odczyt danych z pliku wejściowego
public static String readDataFromFile(String inputFullPath) {
var vsLine = "";
try {
// Odczytanie wszystkich linii z pliku
List<String> lines = Files.readAllLines(Paths.get(inputFullPath));
// Odczytanie i wyświetlenie tylko pierwszej linii
if (!lines.isEmpty()) {
vsLine = lines.get(0);
} else {
System.out.println("Plik jest pusty.");
}
//check
//System.out.println("File content: " + vsLine);
} catch (IOException e) {
// Obsługa błędów odczytu pliku
System.err.println("Wystąpił błąd podczas odczytu pliku: " + e.getMessage());
}
return vsLine;
}
// Zapis danych do pliku wyjściowego - nazwa pliku wyjscioweg, string
public static void writeDataToFile(String outFile, String content) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outFile))) {
writer.write(content.toString());
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
}
public static String getManufacturerShort(int[] manufacturer) {
int temp = (manufacturer[1] << 8) + manufacturer[0];
char[] shortName = new char[4];
shortName[0] = (char) (((temp >> 10) & 0x001F) + 64);
shortName[1] = (char) (((temp >> 5) & 0x001F) + 64);
shortName[2] = (char) ((temp & 0x001F) + 64);
shortName[3] = 0;
return new String(shortName).trim();
}
public static String getEncryptedData(int offset, int size, byte[] rawDataByte) {
byte[] bytes = new byte[size];
for (int i = 0; i < size; i++) {
bytes[i] = rawDataByte[i + offset];
}
return byteArrayToHex(bytes);
}
public static String swapBytes(String hex) {
StringBuilder swappedHex = new StringBuilder();
for (int i = 0; i < hex.length(); i += 2) {
swappedHex.insert(0, hex.substring(i, i + 2));
}
return swappedHex.toString();
}
public static byte[] hexToByteArray(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
public static String byteArrayToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static String[] bytesArrayToHexArray(byte[] byteArray) {
String[] hexArray = new String[byteArray.length];
for (int i = 0; i < byteArray.length; i++) {
hexArray[i] = String.format("%02X", byteArray[i]);
}
return hexArray;
}
//date & time decode
public static String getTimeStamp(String[] hexData) {
// Przykładowa tablica 6 stringów hex
// Dane heksadecymalne M-Bus
// do celów testowych przykład musi dać wynik wynik: 07:54:00 10-12-2024
//String[] hexData = {"80", "76", "07", "0A", "3C", "40"};
/*
for (String hex : hexData) {
System.out.print(hex);
}
*/
// Konwersja tablicy stringów hex na tablicę bajtów
int[] data = new int[hexData.length];
for (int i = 0; i < hexData.length; i++) {
data[i] = (int) Integer.parseInt(hexData[i], 16);
}
String vsSec = String.format("%02d", Integer.parseInt(Integer.toString(data[0] - 128)));
String vsMin = String.format("%02d", Integer.parseInt(Integer.toString(data[1] - 64)));
String vsHou = String.format("%02d", Integer.parseInt(Integer.toString(data[2])));
String vsDay = String.format("%02d", Integer.parseInt(Integer.toString(data[3])));
String vsMon = String.format("%02d", Integer.parseInt(Integer.toString(data[4] - 48)));
String vsYear = String.format("%04d", Integer.parseInt(Integer.toString(data[5] + 1960)));
String vsTimeStamp = vsYear + "-" + vsMon + "-" + vsDay + "T" + vsHou + ":" + vsMin + ":" + vsSec;
//System.out.printf(" - Decoded Date and Time: " + vsTimeStamp);
return vsTimeStamp;
}
public static int bcdToInt(byte bcd) {
return ((bcd >> 4) & 0xF) * 10 + (bcd & 0xF);
}
public static byte[] decryptAES128CBC(String encryptedData, String hexKey, String iv) throws Exception {
byte[] keyBytes = hexToByteArray(hexKey);
byte[] ivBytes = hexToByteArray(iv);
byte[] encryptedBytes = hexToByteArray(encryptedData);
SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] decryptedBytes = cipher.doFinal(encryptedBytes);
// zwraca jako string HEX
//return byteArrayToHex(decryptedBytes);
return (decryptedBytes);
}
}