Skip to content

Commit

Permalink
[dsmr] Handle cosem values that a have a unit or not, related to smar…
Browse files Browse the repository at this point in the history
…ty meters (openhab#8383)

* [dsmr] Smarty fix for decrypting different length Initialization Vector
* [dsmr] Changed specific gitattributes to eol

The telegram files have a checksum that includes the newlines. So it's important the specific newline endings always remain the same. The protocol uses windows line endings, therefor fixed to windows line endings. It was previously set to binary, but that makes it difficult to textual compare with git as it doesn't compare binary files.

* [dsmr] Handle specific cosem values that can either have a unit or not

It appears some Luxembourg meters have a unit in the cosem value, while other meters don't report this value. This changes makes it possible to handle both cases.

Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
  • Loading branch information
Hilbrand authored and markus7017 committed Sep 18, 2020
1 parent f6903cb commit 052ab3d
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 17 deletions.
Expand Up @@ -62,19 +62,21 @@ private enum State {
private static final int ADD_LENGTH = 17;
private static final String ADD = "3000112233445566778899AABBCCDDEEFF";
private static final byte[] ADD_DECODED = HexUtils.hexToBytes(ADD);
private static final int IV_BUFFER_LENGTH = 40;
private static final int GCM_TAG_LENGTH = 12;
private static final int GCM_BITS = GCM_TAG_LENGTH * Byte.SIZE;
private static final int MESSAGES_BUFFER_SIZE = 4096;

private final Logger logger = LoggerFactory.getLogger(SmartyDecrypter.class);
private final ByteBuffer iv = ByteBuffer.allocate(GCM_TAG_LENGTH);
private final ByteBuffer iv = ByteBuffer.allocate(IV_BUFFER_LENGTH);
private final ByteBuffer cipherText = ByteBuffer.allocate(MESSAGES_BUFFER_SIZE);
private final TelegramParser parser;
private @Nullable final SecretKeySpec secretKeySpec;

private State state = State.WAITING_FOR_START_BYTE;
private int currentBytePosition;
private int changeToNextStateAt;
private int ivLength;
private int dataLength;
private boolean lenientMode;
private P1TelegramListener telegramListener;
Expand All @@ -86,14 +88,15 @@ private enum State {
* @param telegramListener
* @param decryptionKey The key to decrypt the messages
*/
public SmartyDecrypter(TelegramParser parser, P1TelegramListener telegramListener, String decryptionKey) {
public SmartyDecrypter(final TelegramParser parser, final P1TelegramListener telegramListener,
final String decryptionKey) {
this.parser = parser;
this.telegramListener = telegramListener;
secretKeySpec = decryptionKey.isEmpty() ? null : new SecretKeySpec(HexUtils.hexToBytes(decryptionKey), "AES");
}

@Override
public void parse(byte[] data, int length) {
public void parse(final byte[] data, final int length) {
for (int i = 0; i < length; i++) {
currentBytePosition++;
if (processStateActions(data[i])) {
Expand All @@ -105,7 +108,7 @@ public void parse(byte[] data, int length) {
}
}

private boolean processStateActions(byte rawInput) {
private boolean processStateActions(final byte rawInput) {
switch (state) {
case WAITING_FOR_START_BYTE:
if (rawInput == START_BYTE) {
Expand All @@ -120,6 +123,7 @@ private boolean processStateActions(byte rawInput) {
break;
case READ_SYSTEM_TITLE:
iv.put(rawInput);
ivLength++;
if (currentBytePosition >= changeToNextStateAt) {
state = State.READ_SEPARATOR_82;
changeToNextStateAt++;
Expand Down Expand Up @@ -154,6 +158,7 @@ private boolean processStateActions(byte rawInput) {
break;
case READ_FRAME_COUNTER:
iv.put(rawInput);
ivLength++;
if (currentBytePosition >= changeToNextStateAt) {
state = State.READ_PAYLOAD;
changeToNextStateAt += dataLength - ADD_LENGTH;
Expand Down Expand Up @@ -182,12 +187,12 @@ private boolean processStateActions(byte rawInput) {
}

private void processCompleted() {
byte[] plainText = decrypt();
final byte[] plainText = decrypt();

reset();
if (plainText == null) {
telegramListener
.telegramReceived(new P1Telegram(Collections.emptyList(), TelegramState.INVALID_ENCRYPTION_KEY));
.telegramReceived(new P1Telegram(Collections.emptyList(), TelegramState.INVALID_ENCRYPTION_KEY));
} else {
parser.parse(plainText, plainText.length);
}
Expand All @@ -201,8 +206,9 @@ private void processCompleted() {
private byte @Nullable [] decrypt() {
try {
if (secretKeySpec != null) {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new GCMParameterSpec(GCM_BITS, iv.array()));
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec,
new GCMParameterSpec(GCM_BITS, iv.array(), 0, ivLength));
cipher.updateAAD(ADD_DECODED);
return cipher.doFinal(cipherText.array(), 0, cipherText.position());
}
Expand All @@ -221,11 +227,12 @@ public void reset() {
cipherText.clear();
currentBytePosition = 0;
changeToNextStateAt = 0;
ivLength = 0;
dataLength = 0;
}

@Override
public void setLenientMode(boolean lenientMode) {
public void setLenientMode(final boolean lenientMode) {
this.lenientMode = lenientMode;
parser.setLenientMode(lenientMode);
}
Expand Down
Expand Up @@ -26,13 +26,21 @@
@NonNullByDefault
class CosemDecimal extends CosemValueDescriptor<DecimalType> {

public static final CosemDecimal INSTANCE = new CosemDecimal();
public static final CosemDecimal INSTANCE = new CosemDecimal(false);
public static final CosemDecimal INSTANCE_WITH_UNITS = new CosemDecimal(true);

private CosemDecimal() {
/**
* If true it can be the input contains a unit. In that case the unit will be stripped before parsing.
*/
private final boolean expectUnit;

private CosemDecimal(boolean expectUnit) {
this.expectUnit = expectUnit;
}

public CosemDecimal(String ohChannelId) {
super(ohChannelId);
this.expectUnit = false;
}

/**
Expand All @@ -45,7 +53,15 @@ public CosemDecimal(String ohChannelId) {
@Override
protected DecimalType getStateValue(String cosemValue) throws ParseException {
try {
return new DecimalType(cosemValue);
final String value;

if (expectUnit) {
final int sep = cosemValue.indexOf('*');
value = sep > 0 ? cosemValue.substring(0, sep) : cosemValue;
} else {
value = cosemValue;
}
return new DecimalType(value);
} catch (NumberFormatException nfe) {
throw new ParseException("Failed to parse value '" + cosemValue + "' as integer", 0);
}
Expand Down
Expand Up @@ -143,9 +143,9 @@ public enum CosemObjectType {
EMETER_TOTAL_IMPORTED_ENERGY_REGISTER_Q(new OBISIdentifier(1, null, 3, 8, 0, null), CosemQuantity.KILO_VAR_HOUR),
EMETER_TOTAL_EXPORTED_ENERGY_REGISTER_Q(new OBISIdentifier(1, null, 4, 8, 0, null), CosemQuantity.KILO_VAR_HOUR),
// The actual reactive's and threshold have no unit in the data and therefore are not quantity types.
EMETER_ACTUAL_REACTIVE_DELIVERY(new OBISIdentifier(1, 0, 3, 7, 0, null), CosemDecimal.INSTANCE),
EMETER_ACTUAL_REACTIVE_PRODUCTION(new OBISIdentifier(1, 0, 4, 7, 0, null), CosemDecimal.INSTANCE),
EMETER_ACTIVE_THRESHOLD_SMAX(new OBISIdentifier(0, 0, 17, 0, 0, null, true), CosemDecimal.INSTANCE),
EMETER_ACTUAL_REACTIVE_DELIVERY(new OBISIdentifier(1, 0, 3, 7, 0, null), CosemDecimal.INSTANCE_WITH_UNITS),
EMETER_ACTUAL_REACTIVE_PRODUCTION(new OBISIdentifier(1, 0, 4, 7, 0, null), CosemDecimal.INSTANCE_WITH_UNITS),
EMETER_ACTIVE_THRESHOLD_SMAX(new OBISIdentifier(0, 0, 17, 0, 0, null, true), CosemDecimal.INSTANCE_WITH_UNITS),
EMETER_INSTANT_REACTIVE_POWER_DELIVERY_L1(new OBISIdentifier(1, 0, 23, 7, 0, null), CosemQuantity.KILO_VAR),
EMETER_INSTANT_REACTIVE_POWER_DELIVERY_L2(new OBISIdentifier(1, 0, 43, 7, 0, null), CosemQuantity.KILO_VAR),
EMETER_INSTANT_REACTIVE_POWER_DELIVERY_L3(new OBISIdentifier(1, 0, 63, 7, 0, null), CosemQuantity.KILO_VAR),
Expand Down
Expand Up @@ -104,7 +104,7 @@ thing-type.config.dsmr.bridgesettings.serialPortAdvanced.description = In het ge
thing-type.config.dsmr.bridgesettings.serialPort.label = Seriële Poort
thing-type.config.dsmr.bridgesettings.serialPort.description = De seriële poort waar de P1 poort van de slimme meter op is aangesloten. (Linux: /dev/ttyUSB0, Windows: COM1)
thing-type.config.dsmr.bridgesettings.receivedTimeout.label = Ontvangst Time-out
thing-type.config.dsmr.bridgesettings.receivedTimeout.description = The De tijdsperiode waarbinnen nieuwe berichten verwacht worden.
thing-type.config.dsmr.bridgesettings.receivedTimeout.description = De tijdsperiode waarbinnen nieuwe berichten verwacht worden.
thing-type.config.dsmr.bridgesettings.baudrate.label = Baudrate
thing-type.config.dsmr.bridgesettings.baudrate.description = De seriële poort baudrate (4800, 9600, 19200, 38400, 57600 of 115200).
thing-type.config.dsmr.bridgesettings.databits.label = Data Bits
Expand Down
Expand Up @@ -47,6 +47,7 @@ public static final List<Object[]> data() {
{ "Landis_Gyr_ZCF110", 25, },
{ "Sagemcom_XS210", 41, },
{ "smarty", 28, },
{ "smarty_with_units", 23, },
});
}
// @formatter:on
Expand Down
Expand Up @@ -59,6 +59,7 @@ public static final List<Object[]> data() {
{ "Landis_Gyr_ZCF110", EnumSet.of( DEVICE_V4, ELECTRICITY_V4_2, M3_V5_0)},
{ "Sagemcom_XS210", EnumSet.of( DEVICE_V4, ELECTRICITY_V4_2)},
{ "smarty", EnumSet.of( DEVICE_V5, ELECTRICITY_SMARTY_V1_0)},
{ "smarty_with_units", EnumSet.of( DEVICE_V5, ELECTRICITY_SMARTY_V1_0, M3_V4)},
});
}
// @formatter:on
Expand Down
@@ -1,2 +1,2 @@
# Always keep the CRLF line endings because the CRC depends on it.
*.telegram binary
*.telegram text eol=crlf
@@ -0,0 +1,25 @@
/Lux5\253663629_D

1-3:0.2.8(42)
0-0:1.0.0(180130102122W)
0-0:42.0.0(53414731303330373030313134303034)
1-0:1.8.0(012345.000*kWh)
1-0:2.8.0(000000.123*kWh)
1-0:3.8.0(000012.345*kvarh)
1-0:4.8.0(001234.567*kvarh)
1-0:1.7.0(01.234*kW)
1-0:2.7.0(00.000*kW)
1-0:3.7.0(00.000*kvar)
1-0:4.7.0(00.000*kvar)
0-0:17.0.0(12.300*kVA)
0-0:96.3.10(1)
0-0:96.13.0()
0-0:96.13.2()
0-0:96.13.3()
0-0:96.13.4()
0-0:96.13.5()
0-1:24.1.0(003)
0-1:96.1.0(1234567890ABCDEF1234567890ABCD)
0-1:24.2.1(200606201530S)(01234.567*m3)
0-1:24.4.0(0)
!3076

0 comments on commit 052ab3d

Please sign in to comment.