[grundfosalpha] Add support for Alpha3 pump (#18187)
* Add support for Alpha3 * Add I18N support for discovery labels Signed-off-by: Jacob Laursen <jacob-github@vindvejr.dk>pull/18293/head
parent
0ccaa3379b
commit
64eeeae32c
|
@ -1,19 +1,44 @@
|
||||||
# GrundfosAlpha Binding
|
# GrundfosAlpha Binding
|
||||||
|
|
||||||
This adds support for reading out the data of Grundfos Alpha Pumps with a [Grundfos Alpha Reader](https://product-selection.grundfos.com/products/alpha-reader)
|
This binding adds support for reading out the data of Grundfos Alpha pumps with a [Grundfos Alpha Reader](https://product-selection.grundfos.com/products/alpha-reader) or [Alpha3 pump](https://product-selection.grundfos.com/products/alpha/alpha3) with built-in Bluetooth.
|
||||||
|
|
||||||
The reverse engineering of the protocol was taken from [https://github.com/JsBergbau/AlphaDecoder](https://github.com/JsBergbau/AlphaDecoder).
|
The reverse engineering of the Alpha Reader protocol was taken from [https://github.com/JsBergbau/AlphaDecoder](https://github.com/JsBergbau/AlphaDecoder).
|
||||||
|
|
||||||
## Supported Things
|
## Supported Things
|
||||||
|
|
||||||
|
- `alpha3`: The Grundfos Alpha3 pump
|
||||||
- `mi401`: The Grundfos MI401 ALPHA Reader
|
- `mi401`: The Grundfos MI401 ALPHA Reader
|
||||||
|
|
||||||
## Discovery
|
## Discovery
|
||||||
|
|
||||||
All readers are auto-detected as soon as Bluetooth is configured in openHAB and the MI401 device is powered on.
|
All pumps and readers are auto-detected as soon as Bluetooth is configured in openHAB and the devices are powered on.
|
||||||
|
|
||||||
## Thing Configuration
|
## Thing Configuration
|
||||||
|
|
||||||
|
### `alpha3` Thing Configuration
|
||||||
|
|
||||||
|
| Name | Type | Description | Default | Required | Advanced |
|
||||||
|
|-----------------|---------|---------------------------------------------------------|---------|----------|----------|
|
||||||
|
| address | text | Bluetooth address in XX:XX:XX:XX:XX:XX format | N/A | yes | no |
|
||||||
|
| refreshInterval | integer | Number of seconds between fetching values from the pump | 30 | no | yes |
|
||||||
|
|
||||||
|
### Pairing
|
||||||
|
|
||||||
|
After creating the Thing, the binding will attempt to connect to the pump.
|
||||||
|
To start the pairing process, press the blue LED button on the pump.
|
||||||
|
When the LED stops blinking and stays lit, the connection has been established, and the Thing should appear online.
|
||||||
|
|
||||||
|
However, the pump may still not be bonded correctly, which could prevent the binding from reconnecting after a disconnection.
|
||||||
|
On Linux, you can take additional steps to fix this issue by manually pairing the pump:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
bluetoothctl pair XX:XX:XX:XX:XX:XX
|
||||||
|
Attempting to pair with XX:XX:XX:XX:XX:XX
|
||||||
|
[CHG] Device XX:XX:XX:XX:XX:XX Bonded: yes
|
||||||
|
[CHG] Device XX:XX:XX:XX:XX:XX Paired: yes
|
||||||
|
Pairing successful
|
||||||
|
```
|
||||||
|
|
||||||
### `mi401` Thing Configuration
|
### `mi401` Thing Configuration
|
||||||
|
|
||||||
| Name | Type | Description | Default | Required | Advanced |
|
| Name | Type | Description | Default | Required | Advanced |
|
||||||
|
@ -22,9 +47,22 @@ All readers are auto-detected as soon as Bluetooth is configured in openHAB and
|
||||||
|
|
||||||
## Channels
|
## Channels
|
||||||
|
|
||||||
|
### `alpha3` Channels
|
||||||
|
|
||||||
| Channel | Type | Read/Write | Description |
|
| Channel | Type | Read/Write | Description |
|
||||||
|------------------|---------------------------|------------|------------------------------------|
|
|------------------|---------------------------|------------|------------------------------------|
|
||||||
| rssi | Number | R | Received Signal Strength Indicator |
|
| rssi | Number:Power | R | Received Signal Strength Indicator |
|
||||||
|
| flow-rate | Number:VolumetricFlowRate | R | The flow rate of the pump |
|
||||||
|
| pump-head | Number:Length | R | The water head above the pump |
|
||||||
|
| voltage-ac | Number:ElectricPotential | R | Current AC pump voltage |
|
||||||
|
| power | Number:Power | R | Current pump power consumption |
|
||||||
|
| motor-speed | Number:Frequency | R | Current rotation of the pump motor |
|
||||||
|
|
||||||
|
### `mi401` Channels
|
||||||
|
|
||||||
|
| Channel | Type | Read/Write | Description |
|
||||||
|
|------------------|---------------------------|------------|------------------------------------|
|
||||||
|
| rssi | Number:Power | R | Received Signal Strength Indicator |
|
||||||
| flow-rate | Number:VolumetricFlowRate | R | The flow rate of the pump |
|
| flow-rate | Number:VolumetricFlowRate | R | The flow rate of the pump |
|
||||||
| pump-head | Number:Length | R | The water head above the pump |
|
| pump-head | Number:Length | R | The water head above the pump |
|
||||||
| pump-temperature | Number:Temperature | R | The temperature of the pump |
|
| pump-temperature | Number:Temperature | R | The temperature of the pump |
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||||
|
import org.openhab.binding.bluetooth.BluetoothDevice;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.protocol.MessageType;
|
||||||
|
import org.openhab.core.util.HexUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This represents a request for writing characteristic to a Bluetooth device.
|
||||||
|
*
|
||||||
|
* This can be used for adding such requests to a queue.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class CharacteristicRequest {
|
||||||
|
private UUID uuid;
|
||||||
|
private byte[] value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new request object.
|
||||||
|
*
|
||||||
|
* @param uuid The UUID of the characteristic
|
||||||
|
* @param messageType The {@link MessageType} containing the data to write
|
||||||
|
*/
|
||||||
|
public CharacteristicRequest(UUID uuid, MessageType messageType) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.value = messageType.request();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the characteristic to the provided {@link BluetoothDevice}.
|
||||||
|
*
|
||||||
|
* @param device The Bluetooth device
|
||||||
|
* @return true if written, false if the characteristic is not found in the device
|
||||||
|
*/
|
||||||
|
public boolean send(BluetoothDevice device) {
|
||||||
|
BluetoothCharacteristic characteristic = device.getCharacteristic(uuid);
|
||||||
|
if (characteristic != null) {
|
||||||
|
device.writeCharacteristic(characteristic, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getUUID() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object o) {
|
||||||
|
if (o == this) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof CharacteristicRequest other)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid.equals(other.uuid) && Arrays.equals(value, other.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(uuid, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return uuid + ": " + HexUtils.bytesToHex(value);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,8 @@
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.bluetooth.grundfosalpha.internal;
|
package org.openhab.binding.bluetooth.grundfosalpha.internal;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
@ -26,11 +28,21 @@ import org.openhab.core.thing.ThingTypeUID;
|
||||||
public class GrundfosAlphaBindingConstants {
|
public class GrundfosAlphaBindingConstants {
|
||||||
|
|
||||||
// List of all Thing Type UIDs
|
// List of all Thing Type UIDs
|
||||||
|
public static final ThingTypeUID THING_TYPE_ALPHA3 = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID,
|
||||||
|
"alpha3");
|
||||||
public static final ThingTypeUID THING_TYPE_MI401 = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "mi401");
|
public static final ThingTypeUID THING_TYPE_MI401 = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "mi401");
|
||||||
|
|
||||||
|
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ALPHA3, THING_TYPE_MI401);
|
||||||
|
|
||||||
|
// List of configuration parameters
|
||||||
|
public static final String CONFIGURATION_REFRESH_INTERVAL = "refreshInterval";
|
||||||
|
|
||||||
// List of all Channel ids
|
// List of all Channel ids
|
||||||
public static final String CHANNEL_TYPE_FLOW_RATE = "flow-rate";
|
public static final String CHANNEL_FLOW_RATE = "flow-rate";
|
||||||
public static final String CHANNEL_TYPE_PUMP_HEAD = "pump-head";
|
public static final String CHANNEL_PUMP_HEAD = "pump-head";
|
||||||
public static final String CHANNEL_TYPE_BATTERY_LEVEL = "battery-level";
|
public static final String CHANNEL_BATTERY_LEVEL = "battery-level";
|
||||||
public static final String CHANNEL_TYPE_PUMP_TEMPERATUR = "pump-temperature";
|
public static final String CHANNEL_PUMP_TEMPERATURE = "pump-temperature";
|
||||||
|
public static final String CHANNEL_VOLTAGE_AC = "voltage-ac";
|
||||||
|
public static final String CHANNEL_POWER = "power";
|
||||||
|
public static final String CHANNEL_MOTOR_SPEED = "motor-speed";
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,10 @@ package org.openhab.binding.bluetooth.grundfosalpha.internal;
|
||||||
|
|
||||||
import static org.openhab.binding.bluetooth.grundfosalpha.internal.GrundfosAlphaBindingConstants.*;
|
import static org.openhab.binding.bluetooth.grundfosalpha.internal.GrundfosAlphaBindingConstants.*;
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.handler.GrundfosAlpha3Handler;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.handler.GrundfosAlphaReaderHandler;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||||
|
@ -35,8 +35,6 @@ import org.osgi.service.component.annotations.Component;
|
||||||
@Component(configurationPid = "binding.bluetooth.grundfosalpha", service = ThingHandlerFactory.class)
|
@Component(configurationPid = "binding.bluetooth.grundfosalpha", service = ThingHandlerFactory.class)
|
||||||
public class GrundfosAlphaHandlerFactory extends BaseThingHandlerFactory {
|
public class GrundfosAlphaHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
|
||||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_MI401);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||||
|
@ -47,7 +45,9 @@ public class GrundfosAlphaHandlerFactory extends BaseThingHandlerFactory {
|
||||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||||
|
|
||||||
if (THING_TYPE_MI401.equals(thingTypeUID)) {
|
if (THING_TYPE_MI401.equals(thingTypeUID)) {
|
||||||
return new GrundfosAlphaHandler(thing);
|
return new GrundfosAlphaReaderHandler(thing);
|
||||||
|
} else if (THING_TYPE_ALPHA3.equals(thingTypeUID)) {
|
||||||
|
return new GrundfosAlpha3Handler(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -10,7 +10,10 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.bluetooth.grundfosalpha.internal;
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.discovery;
|
||||||
|
|
||||||
|
import static org.openhab.binding.bluetooth.BluetoothBindingConstants.*;
|
||||||
|
import static org.openhab.binding.bluetooth.grundfosalpha.internal.GrundfosAlphaBindingConstants.*;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -18,15 +21,18 @@ import java.util.Set;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
|
||||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
|
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
|
||||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
|
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryParticipant;
|
||||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||||
|
import org.openhab.core.i18n.TranslationProvider;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
import org.openhab.core.thing.ThingUID;
|
import org.openhab.core.thing.ThingUID;
|
||||||
|
import org.osgi.framework.FrameworkUtil;
|
||||||
|
import org.osgi.service.component.annotations.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
import org.osgi.service.component.annotations.Component;
|
||||||
|
import org.osgi.service.component.annotations.Reference;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -34,16 +40,23 @@ import org.slf4j.LoggerFactory;
|
||||||
* This discovery participant is able to recognize Grundfos Alpha devices and create discovery results for them.
|
* This discovery participant is able to recognize Grundfos Alpha devices and create discovery results for them.
|
||||||
*
|
*
|
||||||
* @author Markus Heberling - Initial contribution
|
* @author Markus Heberling - Initial contribution
|
||||||
*
|
* @author Jacob Laursen - Added support for Alpha3
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
@Component
|
@Component
|
||||||
public class GrundfosAlphaDiscoveryParticipant implements BluetoothDiscoveryParticipant {
|
public class GrundfosAlphaDiscoveryParticipant implements BluetoothDiscoveryParticipant {
|
||||||
private final Logger logger = LoggerFactory.getLogger(GrundfosAlphaDiscoveryParticipant.class);
|
private final Logger logger = LoggerFactory.getLogger(GrundfosAlphaDiscoveryParticipant.class);
|
||||||
|
|
||||||
|
private final TranslationProvider translationProvider;
|
||||||
|
|
||||||
|
@Activate
|
||||||
|
public GrundfosAlphaDiscoveryParticipant(final @Reference TranslationProvider translationProvider) {
|
||||||
|
this.translationProvider = translationProvider;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||||
return Set.of(GrundfosAlphaBindingConstants.THING_TYPE_MI401);
|
return SUPPORTED_THING_TYPES_UIDS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -54,15 +67,26 @@ public class GrundfosAlphaDiscoveryParticipant implements BluetoothDiscoveryPart
|
||||||
@Override
|
@Override
|
||||||
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
|
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
|
||||||
Integer manufacturerId = device.getManufacturerId();
|
Integer manufacturerId = device.getManufacturerId();
|
||||||
@Nullable
|
|
||||||
String name = device.getName();
|
String name = device.getName();
|
||||||
logger.debug("Discovered device {} with manufacturerId {} and name {}", device.getAddress(), manufacturerId,
|
logger.debug("Discovered device {} with manufacturerId {} and name {}", device.getAddress(), manufacturerId,
|
||||||
name);
|
name);
|
||||||
if ("MI401".equals(name)) {
|
|
||||||
return new ThingUID(GrundfosAlphaBindingConstants.THING_TYPE_MI401, device.getAdapter().getUID(),
|
if (name == null) {
|
||||||
device.getAddress().toString().toLowerCase().replace(":", ""));
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
ThingTypeUID thingTypeUID = switch (name) {
|
||||||
|
case "Alpha3" -> THING_TYPE_ALPHA3;
|
||||||
|
case "MI401" -> THING_TYPE_MI401;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (thingTypeUID == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ThingUID(thingTypeUID, device.getAdapter().getUID(),
|
||||||
|
device.getAddress().toString().toLowerCase().replace(":", ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -71,18 +95,25 @@ public class GrundfosAlphaDiscoveryParticipant implements BluetoothDiscoveryPart
|
||||||
if (thingUID == null) {
|
if (thingUID == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String label = "Grundfos Alpha Reader MI401";
|
|
||||||
|
String thingID = thingUID.getAsString().split(ThingUID.SEPARATOR)[1];
|
||||||
|
String label = translationProvider.getText(FrameworkUtil.getBundle(getClass()),
|
||||||
|
"discovery.%s.label".formatted(thingID), null, null);
|
||||||
|
|
||||||
Map<String, Object> properties = new HashMap<>();
|
Map<String, Object> properties = new HashMap<>();
|
||||||
properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
|
properties.put(CONFIGURATION_ADDRESS, device.getAddress().toString());
|
||||||
properties.put(Thing.PROPERTY_VENDOR, "Grundfos");
|
String deviceName = device.getName();
|
||||||
|
if (deviceName != null) {
|
||||||
|
properties.put(Thing.PROPERTY_MODEL_ID, deviceName);
|
||||||
|
}
|
||||||
Integer txPower = device.getTxPower();
|
Integer txPower = device.getTxPower();
|
||||||
if (txPower != null) {
|
if (txPower != null) {
|
||||||
properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
|
properties.put(PROPERTY_TXPOWER, Integer.toString(txPower));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the discovery result and add to the inbox
|
// Create the discovery result and add to the inbox
|
||||||
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
return DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||||
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS)
|
.withRepresentationProperty(CONFIGURATION_ADDRESS).withBridge(device.getAdapter().getUID())
|
||||||
.withBridge(device.getAdapter().getUID()).withLabel(label).build();
|
.withLabel(label).build();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,285 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.handler;
|
||||||
|
|
||||||
|
import static org.openhab.binding.bluetooth.grundfosalpha.internal.GrundfosAlphaBindingConstants.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.Lock;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||||
|
import org.openhab.binding.bluetooth.BluetoothDevice;
|
||||||
|
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||||
|
import org.openhab.binding.bluetooth.BluetoothService;
|
||||||
|
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.CharacteristicRequest;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.protocol.MessageType;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.protocol.ResponseMessage;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.protocol.SensorDataType;
|
||||||
|
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||||
|
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||||
|
import org.openhab.core.library.types.QuantityType;
|
||||||
|
import org.openhab.core.library.unit.SIUnits;
|
||||||
|
import org.openhab.core.library.unit.Units;
|
||||||
|
import org.openhab.core.thing.ChannelUID;
|
||||||
|
import org.openhab.core.thing.Thing;
|
||||||
|
import org.openhab.core.thing.ThingStatus;
|
||||||
|
import org.openhab.core.types.Command;
|
||||||
|
import org.openhab.core.types.RefreshType;
|
||||||
|
import org.openhab.core.util.HexUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link GrundfosAlpha3Handler} is responsible for handling commands, which are
|
||||||
|
* sent to one of the channels.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class GrundfosAlpha3Handler extends ConnectedBluetoothHandler {
|
||||||
|
|
||||||
|
private static final UUID UUID_SERVICE_GENI = UUID.fromString("0000fe5d-0000-1000-8000-00805f9b34fb");
|
||||||
|
private static final UUID UUID_CHARACTERISTIC_GENI = UUID.fromString("859cffd1-036e-432a-aa28-1a0085b87ba9");
|
||||||
|
|
||||||
|
private static final Set<String> FLOW_HEAD_CHANNELS = Set.of(CHANNEL_FLOW_RATE, CHANNEL_PUMP_HEAD);
|
||||||
|
private static final Set<String> POWER_CHANNELS = Set.of(CHANNEL_VOLTAGE_AC, CHANNEL_POWER, CHANNEL_MOTOR_SPEED);
|
||||||
|
|
||||||
|
private static final int DEFAULT_REFRESH_INTERVAL_SECONDS = 30;
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(GrundfosAlpha3Handler.class);
|
||||||
|
private final Lock stateLock = new ReentrantLock();
|
||||||
|
private final BlockingQueue<CharacteristicRequest> sendQueue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
private @Nullable ScheduledFuture<?> refreshFuture;
|
||||||
|
private @Nullable WriteCharacteristicThread senderThread;
|
||||||
|
private ResponseMessage responseMessage = new ResponseMessage();
|
||||||
|
|
||||||
|
public GrundfosAlpha3Handler(Thing thing) {
|
||||||
|
super(thing);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize() {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
WriteCharacteristicThread senderThread = this.senderThread = new WriteCharacteristicThread(device);
|
||||||
|
senderThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dispose() {
|
||||||
|
WriteCharacteristicThread senderThread = this.senderThread;
|
||||||
|
if (senderThread != null) {
|
||||||
|
senderThread.interrupt();
|
||||||
|
this.senderThread = null;
|
||||||
|
}
|
||||||
|
cancelFuture();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void scheduleFuture() {
|
||||||
|
cancelFuture();
|
||||||
|
|
||||||
|
Object refreshIntervalRaw = getConfig().get(CONFIGURATION_REFRESH_INTERVAL);
|
||||||
|
int refreshInterval = DEFAULT_REFRESH_INTERVAL_SECONDS;
|
||||||
|
if (refreshIntervalRaw instanceof Number number) {
|
||||||
|
refreshInterval = number.intValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshFuture = scheduler.scheduleWithFixedDelay(this::refreshChannels, 0, refreshInterval, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelFuture() {
|
||||||
|
ScheduledFuture<?> refreshFuture = this.refreshFuture;
|
||||||
|
if (refreshFuture != null) {
|
||||||
|
refreshFuture.cancel(true);
|
||||||
|
this.refreshFuture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
|
super.handleCommand(channelUID, command);
|
||||||
|
|
||||||
|
if (command != RefreshType.REFRESH) {
|
||||||
|
// Currently no writable channels
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||||
|
logger.info("Cannot send command {} because device is not connected", command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FLOW_HEAD_CHANNELS.contains(channelUID.getId())) {
|
||||||
|
sendQueue.add(new CharacteristicRequest(UUID_CHARACTERISTIC_GENI, MessageType.FlowHead));
|
||||||
|
} else if (POWER_CHANNELS.contains(channelUID.getId())) {
|
||||||
|
sendQueue.add(new CharacteristicRequest(UUID_CHARACTERISTIC_GENI, MessageType.Power));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onServicesDiscovered() {
|
||||||
|
logger.debug("onServicesDiscovered");
|
||||||
|
super.onServicesDiscovered();
|
||||||
|
|
||||||
|
for (BluetoothService service : device.getServices()) {
|
||||||
|
logger.debug("Supported service for {}: {}", device.getName(), service.getUuid());
|
||||||
|
if (UUID_SERVICE_GENI.equals(service.getUuid())) {
|
||||||
|
List<BluetoothCharacteristic> characteristics = service.getCharacteristics();
|
||||||
|
for (BluetoothCharacteristic characteristic : characteristics) {
|
||||||
|
if (UUID_CHARACTERISTIC_GENI.equals(characteristic.getUuid())) {
|
||||||
|
logger.debug("Characteristic {} found for service {}", UUID_CHARACTERISTIC_GENI,
|
||||||
|
UUID_SERVICE_GENI);
|
||||||
|
device.enableNotifications(characteristic);
|
||||||
|
String deviceName = device.getName();
|
||||||
|
if (deviceName != null) {
|
||||||
|
updateProperty(Thing.PROPERTY_MODEL_ID, deviceName);
|
||||||
|
}
|
||||||
|
updateStatus(ThingStatus.ONLINE);
|
||||||
|
scheduleFuture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
||||||
|
logger.debug("onScanRecordReceived");
|
||||||
|
|
||||||
|
// Avoid calling super method when it would set Thing status to online.
|
||||||
|
// Instead, we set the status in onServicesDiscovered when we have discovered
|
||||||
|
// the GENI service and characteristic, which means we have a pairing connection.
|
||||||
|
if (Integer.MIN_VALUE != scanNotification.getRssi()) {
|
||||||
|
super.onScanRecordReceived(scanNotification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] value) {
|
||||||
|
super.onCharacteristicUpdate(characteristic, value);
|
||||||
|
|
||||||
|
stateLock.lock();
|
||||||
|
try {
|
||||||
|
if (!UUID_CHARACTERISTIC_GENI.equals(characteristic.getUuid())) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Received update {} to unknown characteristic {} of device {}",
|
||||||
|
HexUtils.bytesToHex(value), characteristic.getUuid(), address);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseMessage.addPacket(value)) {
|
||||||
|
Map<SensorDataType, BigDecimal> values = responseMessage.decode();
|
||||||
|
updateChannels(values);
|
||||||
|
responseMessage = new ResponseMessage();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stateLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshChannels() {
|
||||||
|
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FLOW_HEAD_CHANNELS.stream().anyMatch(this::isLinked)) {
|
||||||
|
sendQueue.add(new CharacteristicRequest(UUID_CHARACTERISTIC_GENI, MessageType.FlowHead));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (POWER_CHANNELS.stream().anyMatch(this::isLinked)) {
|
||||||
|
sendQueue.add(new CharacteristicRequest(UUID_CHARACTERISTIC_GENI, MessageType.Power));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChannels(Map<SensorDataType, BigDecimal> values) {
|
||||||
|
for (Entry<SensorDataType, BigDecimal> entry : values.entrySet()) {
|
||||||
|
BigDecimal stateValue = entry.getValue();
|
||||||
|
switch (entry.getKey()) {
|
||||||
|
case Flow -> updateState(CHANNEL_FLOW_RATE, new QuantityType<>(stateValue, Units.CUBICMETRE_PER_HOUR));
|
||||||
|
case Head -> updateState(CHANNEL_PUMP_HEAD, new QuantityType<>(stateValue, SIUnits.METRE));
|
||||||
|
case VoltageAC -> updateState(CHANNEL_VOLTAGE_AC, new QuantityType<>(stateValue, Units.VOLT));
|
||||||
|
case PowerConsumption -> updateState(CHANNEL_POWER, new QuantityType<>(stateValue, Units.WATT));
|
||||||
|
case MotorSpeed -> updateState(CHANNEL_MOTOR_SPEED, new QuantityType<>(stateValue, Units.RPM));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
|
||||||
|
super.onConnectionStateChange(connectionNotification);
|
||||||
|
logger.debug("{}", connectionNotification.getConnectionState());
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WriteCharacteristicThread extends Thread {
|
||||||
|
private static final int REQUEST_DELAY_MS = 700;
|
||||||
|
|
||||||
|
private final BluetoothDevice device;
|
||||||
|
|
||||||
|
public WriteCharacteristicThread(BluetoothDevice device) {
|
||||||
|
super("OH-binding-" + getThing().getUID() + "-WriteCharacteristicThread");
|
||||||
|
this.device = device;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
logger.debug("Starting sender thread");
|
||||||
|
while (!interrupted()) {
|
||||||
|
try {
|
||||||
|
processQueue();
|
||||||
|
Thread.sleep(REQUEST_DELAY_MS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Sender thread finished");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processQueue() throws InterruptedException {
|
||||||
|
logger.trace("Processing/await queue, size: {}", sendQueue.size());
|
||||||
|
CharacteristicRequest request = sendQueue.take();
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Writing characteristic {}: {}", request.getUUID(),
|
||||||
|
HexUtils.bytesToHex(request.getValue()));
|
||||||
|
}
|
||||||
|
if (request.send(device)) {
|
||||||
|
removeDuplicates(request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeDuplicates(CharacteristicRequest request) {
|
||||||
|
int duplicates = 0;
|
||||||
|
while (sendQueue.remove(request)) {
|
||||||
|
duplicates++;
|
||||||
|
}
|
||||||
|
if (duplicates > 0 && logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Removed {} duplicate characteristic requests for '{}' from queue", duplicates, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.bluetooth.grundfosalpha.internal;
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.handler;
|
||||||
|
|
||||||
import static org.openhab.binding.bluetooth.grundfosalpha.internal.GrundfosAlphaBindingConstants.*;
|
import static org.openhab.binding.bluetooth.grundfosalpha.internal.GrundfosAlphaBindingConstants.*;
|
||||||
|
|
||||||
|
@ -20,28 +20,23 @@ import javax.measure.quantity.Temperature;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
||||||
import org.openhab.binding.bluetooth.BluetoothDeviceListener;
|
|
||||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||||
import org.openhab.core.library.dimension.VolumetricFlowRate;
|
import org.openhab.core.library.dimension.VolumetricFlowRate;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
import org.openhab.core.library.unit.SIUnits;
|
import org.openhab.core.library.unit.SIUnits;
|
||||||
import org.openhab.core.library.unit.Units;
|
import org.openhab.core.library.unit.Units;
|
||||||
import org.openhab.core.thing.Thing;
|
import org.openhab.core.thing.Thing;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The {@link GrundfosAlphaHandler} is responsible for handling commands, which are
|
* The {@link GrundfosAlphaReaderHandler} is responsible for handling commands, which are
|
||||||
* sent to one of the channels.
|
* sent to one of the channels.
|
||||||
*
|
*
|
||||||
* @author Markus Heberling - Initial contribution
|
* @author Markus Heberling - Initial contribution
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class GrundfosAlphaHandler extends BeaconBluetoothHandler implements BluetoothDeviceListener {
|
public class GrundfosAlphaReaderHandler extends BeaconBluetoothHandler {
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(GrundfosAlphaHandler.class);
|
public GrundfosAlphaReaderHandler(Thing thing) {
|
||||||
|
|
||||||
public GrundfosAlphaHandler(Thing thing) {
|
|
||||||
super(thing);
|
super(thing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,22 +44,22 @@ public class GrundfosAlphaHandler extends BeaconBluetoothHandler implements Blue
|
||||||
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
||||||
super.onScanRecordReceived(scanNotification);
|
super.onScanRecordReceived(scanNotification);
|
||||||
byte[] data = scanNotification.getManufacturerData();
|
byte[] data = scanNotification.getManufacturerData();
|
||||||
if (data != null && data.length == 21) {
|
if (data.length == 21) {
|
||||||
int batteryLevel = (data[5] & 0xFF) * 25;
|
int batteryLevel = (data[5] & 0xFF) * 25;
|
||||||
QuantityType<Dimensionless> quantity = new QuantityType<>(batteryLevel, Units.PERCENT);
|
QuantityType<Dimensionless> quantity = new QuantityType<>(batteryLevel, Units.PERCENT);
|
||||||
updateState(CHANNEL_TYPE_BATTERY_LEVEL, quantity);
|
updateState(CHANNEL_BATTERY_LEVEL, quantity);
|
||||||
|
|
||||||
float flowRate = ((data[9] & 0xFF) << 8 | (data[8] & 0xFF)) / 6553.5f;
|
float flowRate = ((data[9] & 0xFF) << 8 | (data[8] & 0xFF)) / 6553.5f;
|
||||||
QuantityType<VolumetricFlowRate> quantity2 = new QuantityType<>(flowRate, Units.CUBICMETRE_PER_HOUR);
|
QuantityType<VolumetricFlowRate> quantity2 = new QuantityType<>(flowRate, Units.CUBICMETRE_PER_HOUR);
|
||||||
updateState(CHANNEL_TYPE_FLOW_RATE, quantity2);
|
updateState(CHANNEL_FLOW_RATE, quantity2);
|
||||||
|
|
||||||
float pumpHead = ((data[11] & 0xFF) << 8 | (data[10] & 0xFF)) / 3276.7f;
|
float pumpHead = ((data[11] & 0xFF) << 8 | (data[10] & 0xFF)) / 3276.7f;
|
||||||
QuantityType<Length> quantity3 = new QuantityType<>(pumpHead, SIUnits.METRE);
|
QuantityType<Length> quantity3 = new QuantityType<>(pumpHead, SIUnits.METRE);
|
||||||
updateState(CHANNEL_TYPE_PUMP_HEAD, quantity3);
|
updateState(CHANNEL_PUMP_HEAD, quantity3);
|
||||||
|
|
||||||
float pumpTemperature = data[14] & 0xFF;
|
float pumpTemperature = data[14] & 0xFF;
|
||||||
QuantityType<Temperature> quantity4 = new QuantityType<>(pumpTemperature, SIUnits.CELSIUS);
|
QuantityType<Temperature> quantity4 = new QuantityType<>(pumpTemperature, SIUnits.CELSIUS);
|
||||||
updateState(CHANNEL_TYPE_PUMP_TEMPERATUR, quantity4);
|
updateState(CHANNEL_PUMP_TEMPERATURE, quantity4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for checksum calculation using the CRC-16-CCITT algorithm.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class CRC16Calculator {
|
||||||
|
|
||||||
|
public static final int[] LOOKUP_TABLE = { 0, 4129, 8258, 12387, 16516, 20645, 24774, 28903, 33032, 37161, 41290,
|
||||||
|
45419, 49548, 53677, 57806, 61935, 4657, 528, 12915, 8786, 21173, 17044, 29431, 25302, 37689, 33560, 45947,
|
||||||
|
41818, 54205, 50076, 62463, 58334, 9314, 13379, 1056, 5121, 25830, 29895, 17572, 21637, 42346, 46411, 34088,
|
||||||
|
38153, 58862, 62927, 50604, 54669, 13907, 9842, 5649, 1584, 30423, 26358, 22165, 18100, 46939, 42874, 38681,
|
||||||
|
34616, 63455, 59390, 55197, 51132, 18628, 22757, 26758, 30887, 2112, 6241, 10242, 14371, 51660, 55789,
|
||||||
|
59790, 63919, 35144, 39273, 43274, 47403, 23285, 19156, 31415, 27286, 6769, 2640, 14899, 10770, 56317,
|
||||||
|
52188, 64447, 60318, 39801, 35672, 47931, 43802, 27814, 31879, 19684, 23749, 11298, 15363, 3168, 7233,
|
||||||
|
60846, 64911, 52716, 56781, 44330, 48395, 36200, 40265, 32407, 28342, 24277, 20212, 15891, 11826, 7761,
|
||||||
|
3696, 65439, 61374, 57309, 53244, 48923, 44858, 40793, 36728, 37256, 33193, 45514, 41451, 53516, 49453,
|
||||||
|
61774, 57711, 4224, 161, 12482, 8419, 20484, 16421, 28742, 24679, 33721, 37784, 41979, 46042, 49981, 54044,
|
||||||
|
58239, 62302, 689, 4752, 8947, 13010, 16949, 21012, 25207, 29270, 46570, 42443, 38312, 34185, 62830, 58703,
|
||||||
|
54572, 50445, 13538, 9411, 5280, 1153, 29798, 25671, 21540, 17413, 42971, 47098, 34713, 38840, 59231, 63358,
|
||||||
|
50973, 55100, 9939, 14066, 1681, 5808, 26199, 30326, 17941, 22068, 55628, 51565, 63758, 59695, 39368, 35305,
|
||||||
|
47498, 43435, 22596, 18533, 30726, 26663, 6336, 2273, 14466, 10403, 52093, 56156, 60223, 64286, 35833,
|
||||||
|
39896, 43963, 48026, 19061, 23124, 27191, 31254, 2801, 6864, 10931, 14994, 64814, 60687, 56684, 52557,
|
||||||
|
48554, 44427, 40424, 36297, 31782, 27655, 23652, 19525, 15522, 11395, 7392, 3265, 61215, 65342, 53085,
|
||||||
|
57212, 44955, 49082, 36825, 40952, 28183, 32310, 20053, 24180, 11923, 16050, 3793, 7920 };
|
||||||
|
|
||||||
|
private static final int CRC16_INITIAL_VALUE = 0xffff;
|
||||||
|
private static final int CRC16_FINAL_XOR = 0xffff;
|
||||||
|
private static final int BYTE_MASK = 0xff;
|
||||||
|
|
||||||
|
private CRC16Calculator() {
|
||||||
|
// Prevent instantiation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the CRC-16 checksum for the given data.
|
||||||
|
*
|
||||||
|
* @param data The data to validate, including checksum bytes.
|
||||||
|
* @return True if the checksum is valid, false otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean check(byte[] data) {
|
||||||
|
if (data.length < 3) {
|
||||||
|
throw new IllegalArgumentException("Data array must contain at least 3 bytes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int dataLength = (data[1] & BYTE_MASK);
|
||||||
|
if (dataLength + 4 > data.length) {
|
||||||
|
throw new IllegalArgumentException("Invalid data length specified in the array.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int crcValue = calculate(data, 1, dataLength + 1);
|
||||||
|
byte lowByte = getLowByte(crcValue);
|
||||||
|
byte highByte = getHighByte(crcValue);
|
||||||
|
|
||||||
|
return data[dataLength + 2] == highByte && data[dataLength + 3] == lowByte;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the CRC-16 checksum for the given data and puts it into
|
||||||
|
* the last two bytes of the given data array.
|
||||||
|
*
|
||||||
|
* @param data The data array to calculate CRC-16 with the two last bytes reserved for the result
|
||||||
|
* @param length The message size excluding start delimiter/size (first two bytes) and CRC-16 checksum (last two
|
||||||
|
* bytes)
|
||||||
|
*/
|
||||||
|
public static void put(byte[] data, int length) {
|
||||||
|
int dataLength = (data[1] & BYTE_MASK);
|
||||||
|
if (dataLength + 4 > data.length) {
|
||||||
|
throw new IllegalArgumentException("Invalid data length specified in the array.");
|
||||||
|
}
|
||||||
|
int crcValue = calculate(data, 1, length + 1);
|
||||||
|
data[length + 2] = getHighByte(crcValue);
|
||||||
|
data[length + 3] = getLowByte(crcValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int calculate(byte[] data, int start, int length) {
|
||||||
|
int value = CRC16_INITIAL_VALUE;
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int inputByte = data[start + i] & BYTE_MASK;
|
||||||
|
int tableIndex = (inputByte ^ (value >> 8)) & BYTE_MASK;
|
||||||
|
value = (LOOKUP_TABLE[tableIndex] ^ (value << 8)) & 0xffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value ^ CRC16_FINAL_XOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte getLowByte(int word) {
|
||||||
|
return (byte) (word & BYTE_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte getHighByte(int word) {
|
||||||
|
return (byte) ((word >> 8) & BYTE_MASK);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This defines the protocol header.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class MessageHeader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header length including {@link MessageStartDelimiter} and size byte.
|
||||||
|
*/
|
||||||
|
public static final int LENGTH = 5;
|
||||||
|
|
||||||
|
private static final byte OFFSET_START_DELIMITER = 0;
|
||||||
|
private static final byte OFFSET_LENGTH = 1;
|
||||||
|
private static final byte OFFSET_SOURCE_ADDRESS = 2;
|
||||||
|
private static final byte OFFSET_DESTINATION_ADDRESS = 3;
|
||||||
|
private static final byte OFFSET_HEADER4 = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address of the controller/client (openHAB).
|
||||||
|
*/
|
||||||
|
private static final byte CONTROLLER_ADDRESS = (byte) 0xe7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address of the peripheral/server (pump).
|
||||||
|
*/
|
||||||
|
private static final byte PERIPHERAL_ADDRESS = (byte) 0xf8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last byte in header used for flowhead/power requests/responses.
|
||||||
|
* Not sure about meaning.
|
||||||
|
*/
|
||||||
|
private static final byte HEADER4_VALUE = (byte) 0x0a;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fill in header for a request.
|
||||||
|
*
|
||||||
|
* @param request Request buffer
|
||||||
|
* @param messageLength The request size excluding {@link MessageStartDelimiter}, size byte and CRC-16 checksum
|
||||||
|
*/
|
||||||
|
public static void setRequestHeader(byte[] request, int messageLength) {
|
||||||
|
if (request.length < LENGTH) {
|
||||||
|
throw new IllegalArgumentException("Buffer is too small for header");
|
||||||
|
}
|
||||||
|
|
||||||
|
request[OFFSET_START_DELIMITER] = MessageStartDelimiter.Request.value();
|
||||||
|
request[OFFSET_LENGTH] = (byte) messageLength;
|
||||||
|
request[OFFSET_SOURCE_ADDRESS] = CONTROLLER_ADDRESS;
|
||||||
|
request[OFFSET_DESTINATION_ADDRESS] = PERIPHERAL_ADDRESS;
|
||||||
|
request[OFFSET_HEADER4] = HEADER4_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this packet is the first packet in a response payload.
|
||||||
|
*
|
||||||
|
* @param packet The packet to inspect
|
||||||
|
* @return true if determined to be first packet, otherwise false
|
||||||
|
*/
|
||||||
|
public static boolean isInitialResponsePacket(byte[] packet) {
|
||||||
|
return packet.length >= LENGTH && packet[OFFSET_START_DELIMITER] == MessageStartDelimiter.Reply.value()
|
||||||
|
&& packet[OFFSET_SOURCE_ADDRESS] == PERIPHERAL_ADDRESS
|
||||||
|
&& packet[OFFSET_DESTINATION_ADDRESS] == CONTROLLER_ADDRESS && packet[OFFSET_HEADER4] == HEADER4_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total size of message including {@link MessageStartDelimiter}, size byte and CRC-16 checksum.
|
||||||
|
*
|
||||||
|
* @param header Header bytes (at least)
|
||||||
|
* @return total size
|
||||||
|
*/
|
||||||
|
public static int getTotalSize(byte[] header) {
|
||||||
|
return ((byte) header[MessageHeader.OFFSET_LENGTH]) + 4;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This defines the start delimiters for different kinds of messages.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public enum MessageStartDelimiter {
|
||||||
|
Reply((byte) 0x24),
|
||||||
|
Message((byte) 0x26),
|
||||||
|
Request((byte) 0x27),
|
||||||
|
Echo((byte) 0x30);
|
||||||
|
|
||||||
|
private final byte value;
|
||||||
|
|
||||||
|
MessageStartDelimiter(byte value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte value() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This represents the different types of messages that can be requested and received.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public enum MessageType {
|
||||||
|
FlowHead(new byte[] { 0x1f, 0x00, 0x01, 0x30, 0x01, 0x00, 0x00, 0x18 },
|
||||||
|
new byte[] { (byte) 0x03, (byte) 0x5d, (byte) 0x01, (byte) 0x21 }),
|
||||||
|
Power(new byte[] { 0x2c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x25 },
|
||||||
|
new byte[] { (byte) 0x03, (byte) 0x57, (byte) 0x00, (byte) 0x45 });
|
||||||
|
|
||||||
|
private final byte[] responseType;
|
||||||
|
private final byte[] request;
|
||||||
|
|
||||||
|
MessageType(byte[] responseType, byte[] requestMessage) {
|
||||||
|
this.responseType = responseType;
|
||||||
|
|
||||||
|
int messageLength = requestMessage.length + 3;
|
||||||
|
request = new byte[messageLength + 4];
|
||||||
|
|
||||||
|
// Append the header
|
||||||
|
MessageHeader.setRequestHeader(request, messageLength);
|
||||||
|
|
||||||
|
// Append the message type-specific part
|
||||||
|
System.arraycopy(requestMessage, 0, request, MessageHeader.LENGTH, requestMessage.length);
|
||||||
|
|
||||||
|
CRC16Calculator.put(request, messageLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] responseType() {
|
||||||
|
return responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] request() {
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.openhab.core.util.HexUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This represents a message received from the Alpha3 pump, made of individual
|
||||||
|
* packets received. Packets are expected to be in correct order, otherwise
|
||||||
|
* they will be skipped.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ResponseMessage {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ResponseMessage.class);
|
||||||
|
|
||||||
|
private static final int GENI_RESPONSE_MAX_SIZE = 259;
|
||||||
|
private static final int GENI_RESPONSE_TYPE_LENGTH = 8;
|
||||||
|
|
||||||
|
private int responseTotalSize;
|
||||||
|
private int responseOffset;
|
||||||
|
private int responseRemaining = Integer.MAX_VALUE;
|
||||||
|
private byte[] response = new byte[GENI_RESPONSE_MAX_SIZE];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add packet from response payload.
|
||||||
|
*
|
||||||
|
* @param packet
|
||||||
|
* @return true if response is now complete
|
||||||
|
*/
|
||||||
|
public boolean addPacket(byte[] packet) {
|
||||||
|
if (logger.isTraceEnabled()) {
|
||||||
|
logger.trace("GENI response: {}", HexUtils.bytesToHex(packet));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFirstPacket = MessageHeader.isInitialResponsePacket(packet);
|
||||||
|
|
||||||
|
if (responseRemaining == Integer.MAX_VALUE) {
|
||||||
|
if (!MessageHeader.isInitialResponsePacket(packet)) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
byte[] header = new byte[MessageHeader.LENGTH];
|
||||||
|
System.arraycopy(packet, 0, header, 0, MessageHeader.LENGTH);
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("Response bytes {} don't match GENI header", HexUtils.bytesToHex(header));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
responseTotalSize = MessageHeader.getTotalSize(packet);
|
||||||
|
responseOffset = 0;
|
||||||
|
responseRemaining = responseTotalSize;
|
||||||
|
} else if (isFirstPacket && responseRemaining > 0) {
|
||||||
|
logger.debug("Received new first packet while awaiting continuation, resetting");
|
||||||
|
|
||||||
|
responseTotalSize = MessageHeader.getTotalSize(packet);
|
||||||
|
responseOffset = 0;
|
||||||
|
responseRemaining = responseTotalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.arraycopy(packet, 0, response, responseOffset, packet.length);
|
||||||
|
responseOffset += packet.length;
|
||||||
|
responseRemaining -= packet.length;
|
||||||
|
|
||||||
|
if (responseRemaining < 0) {
|
||||||
|
responseRemaining = Integer.MAX_VALUE;
|
||||||
|
responseOffset = 0;
|
||||||
|
logger.debug("Received too many bytes");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseRemaining == 0) {
|
||||||
|
if (!CRC16Calculator.check(response)) {
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("CRC16 check failed for {}", HexUtils.bytesToHex(response));
|
||||||
|
}
|
||||||
|
responseRemaining = Integer.MAX_VALUE;
|
||||||
|
responseOffset = 0;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<SensorDataType, BigDecimal> decode() {
|
||||||
|
HashMap<SensorDataType, BigDecimal> values = new HashMap<>();
|
||||||
|
|
||||||
|
for (SensorDataType dataType : SensorDataType.values()) {
|
||||||
|
decode(dataType).ifPresent(value -> values.put(dataType, new BigDecimal(value.getFloat())
|
||||||
|
.multiply(dataType.factor()).setScale(dataType.decimals(), RoundingMode.HALF_UP)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ByteBuffer> decode(SensorDataType dataType) {
|
||||||
|
byte[] expectedResponseType = dataType.messageType().responseType();
|
||||||
|
byte[] responseType = new byte[GENI_RESPONSE_TYPE_LENGTH];
|
||||||
|
System.arraycopy(response, MessageHeader.LENGTH, responseType, 0, GENI_RESPONSE_TYPE_LENGTH);
|
||||||
|
|
||||||
|
if (!Arrays.equals(expectedResponseType, responseType)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
int valueOffset = MessageHeader.LENGTH + GENI_RESPONSE_TYPE_LENGTH + dataType.offset();
|
||||||
|
byte[] valueBuffer = Arrays.copyOfRange(response, valueOffset, valueOffset + 4);
|
||||||
|
|
||||||
|
return Optional.of(ByteBuffer.wrap(valueBuffer).order(ByteOrder.BIG_ENDIAN));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This represents the different sensor data types that can
|
||||||
|
* be extracted from a {@link ResponseMessage}.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public enum SensorDataType {
|
||||||
|
Flow(MessageType.FlowHead, 0, new BigDecimal(3600), 3),
|
||||||
|
Head(MessageType.FlowHead, 4, new BigDecimal("0.0001"), 5),
|
||||||
|
VoltageAC(MessageType.Power, 0, BigDecimal.ONE, 1),
|
||||||
|
PowerConsumption(MessageType.Power, 12, BigDecimal.ONE, 1),
|
||||||
|
MotorSpeed(MessageType.Power, 20, BigDecimal.ONE, 0);
|
||||||
|
|
||||||
|
private final MessageType messageType;
|
||||||
|
private final int offset;
|
||||||
|
private final BigDecimal factor;
|
||||||
|
private final int decimals;
|
||||||
|
|
||||||
|
SensorDataType(MessageType messageType, int offset, BigDecimal factor, int decimals) {
|
||||||
|
this.messageType = messageType;
|
||||||
|
this.offset = offset;
|
||||||
|
this.factor = factor;
|
||||||
|
this.decimals = decimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MessageType messageType() {
|
||||||
|
return messageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int offset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal factor() {
|
||||||
|
return factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int decimals() {
|
||||||
|
return decimals;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,20 @@
|
||||||
# thing types
|
# thing types
|
||||||
|
|
||||||
|
thing-type.bluetooth.alpha3.label = Grundfos Alpha3
|
||||||
|
thing-type.bluetooth.alpha3.description = A Grundfos Alpha3 circulator pump
|
||||||
|
thing-type.bluetooth.alpha3.channel.power.label = Power Consumption
|
||||||
|
thing-type.bluetooth.alpha3.channel.power.description = Current pump power consumption
|
||||||
|
thing-type.bluetooth.alpha3.channel.voltage-ac.label = AC Voltage
|
||||||
|
thing-type.bluetooth.alpha3.channel.voltage-ac.description = Current AC pump voltage
|
||||||
thing-type.bluetooth.mi401.label = Grundfos Alpha Reader MI401
|
thing-type.bluetooth.mi401.label = Grundfos Alpha Reader MI401
|
||||||
thing-type.bluetooth.mi401.description = A Grundfos Alpha Reader MI401
|
thing-type.bluetooth.mi401.description = A Grundfos Alpha Reader MI401
|
||||||
|
|
||||||
# thing types config
|
# thing types config
|
||||||
|
|
||||||
|
thing-type.config.bluetooth.alpha3.address.label = Address
|
||||||
|
thing-type.config.bluetooth.alpha3.address.description = Bluetooth address in XX:XX:XX:XX:XX:XX format
|
||||||
|
thing-type.config.bluetooth.alpha3.refreshInterval.label = Refresh Interval
|
||||||
|
thing-type.config.bluetooth.alpha3.refreshInterval.description = Number of seconds between fetching values from the pump. Default is 30
|
||||||
thing-type.config.bluetooth.mi401.address.label = Address
|
thing-type.config.bluetooth.mi401.address.label = Address
|
||||||
thing-type.config.bluetooth.mi401.address.description = Bluetooth address in XX:XX:XX:XX:XX:XX format
|
thing-type.config.bluetooth.mi401.address.description = Bluetooth address in XX:XX:XX:XX:XX:XX format
|
||||||
|
|
||||||
|
@ -14,5 +24,12 @@ channel-type.bluetooth.grundfos-flow.label = Current Flow
|
||||||
channel-type.bluetooth.grundfos-flow.description = Current flow
|
channel-type.bluetooth.grundfos-flow.description = Current flow
|
||||||
channel-type.bluetooth.grundfos-head.label = Current Head
|
channel-type.bluetooth.grundfos-head.label = Current Head
|
||||||
channel-type.bluetooth.grundfos-head.description = Current head
|
channel-type.bluetooth.grundfos-head.description = Current head
|
||||||
|
channel-type.bluetooth.grundfos-motor-speed.label = Motor Speed
|
||||||
|
channel-type.bluetooth.grundfos-motor-speed.description = Current rotation of the pump motor
|
||||||
channel-type.bluetooth.grundfos-temperature.label = Current Pump Temperature
|
channel-type.bluetooth.grundfos-temperature.label = Current Pump Temperature
|
||||||
channel-type.bluetooth.grundfos-temperature.description = Current pump temperature
|
channel-type.bluetooth.grundfos-temperature.description = Current pump temperature
|
||||||
|
|
||||||
|
# discovery result
|
||||||
|
|
||||||
|
discovery.alpha3.label = Grundfos Alpha3 Circulator Pump
|
||||||
|
discovery.mi401.label = Grundfos Alpha Reader MI401
|
||||||
|
|
|
@ -32,6 +32,14 @@
|
||||||
<state readOnly="true" pattern="%.2f %unit%"/>
|
<state readOnly="true" pattern="%.2f %unit%"/>
|
||||||
</channel-type>
|
</channel-type>
|
||||||
|
|
||||||
|
<channel-type id="grundfos-motor-speed">
|
||||||
|
<item-type unitHint="rpm">Number:Frequency</item-type>
|
||||||
|
<label>Motor Speed</label>
|
||||||
|
<description>Current rotation of the pump motor</description>
|
||||||
|
<category>Fan</category>
|
||||||
|
<state readOnly="true" pattern="%.0f rpm"/>
|
||||||
|
</channel-type>
|
||||||
|
|
||||||
<thing-type id="mi401">
|
<thing-type id="mi401">
|
||||||
<supported-bridge-type-refs>
|
<supported-bridge-type-refs>
|
||||||
<bridge-type-ref id="roaming"/>
|
<bridge-type-ref id="roaming"/>
|
||||||
|
@ -51,12 +59,65 @@
|
||||||
<channel id="battery-level" typeId="system.battery-level"/>
|
<channel id="battery-level" typeId="system.battery-level"/>
|
||||||
</channels>
|
</channels>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<property name="vendor">Grundfos</property>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<representation-property>address</representation-property>
|
||||||
|
|
||||||
<config-description>
|
<config-description>
|
||||||
<parameter name="address" type="text">
|
<parameter name="address" type="text">
|
||||||
<label>Address</label>
|
<label>Address</label>
|
||||||
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||||
</parameter>
|
</parameter>
|
||||||
</config-description>
|
</config-description>
|
||||||
|
|
||||||
</thing-type>
|
</thing-type>
|
||||||
|
|
||||||
|
<thing-type id="alpha3">
|
||||||
|
<supported-bridge-type-refs>
|
||||||
|
<bridge-type-ref id="roaming"/>
|
||||||
|
<bridge-type-ref id="bluegiga"/>
|
||||||
|
<bridge-type-ref id="bluez"/>
|
||||||
|
</supported-bridge-type-refs>
|
||||||
|
|
||||||
|
<label>Grundfos Alpha3</label>
|
||||||
|
<description>A Grundfos Alpha3 circulator pump</description>
|
||||||
|
<category>Pump</category>
|
||||||
|
|
||||||
|
<channels>
|
||||||
|
<channel id="rssi" typeId="rssi"/>
|
||||||
|
<channel id="flow-rate" typeId="grundfos-flow"/>
|
||||||
|
<channel id="pump-head" typeId="grundfos-head"/>
|
||||||
|
<channel id="voltage-ac" typeId="system.electric-voltage">
|
||||||
|
<label>AC Voltage</label>
|
||||||
|
<description>Current AC pump voltage</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="power" typeId="system.electric-power">
|
||||||
|
<label>Power Consumption</label>
|
||||||
|
<description>Current pump power consumption</description>
|
||||||
|
</channel>
|
||||||
|
<channel id="motor-speed" typeId="grundfos-motor-speed"/>
|
||||||
|
</channels>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<property name="vendor">Grundfos</property>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<representation-property>address</representation-property>
|
||||||
|
|
||||||
|
<config-description>
|
||||||
|
<parameter name="address" type="text">
|
||||||
|
<label>Address</label>
|
||||||
|
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||||
|
</parameter>
|
||||||
|
<parameter name="refreshInterval" type="integer" min="5" required="false" unit="s">
|
||||||
|
<default>30</default>
|
||||||
|
<label>Refresh Interval</label>
|
||||||
|
<description>Number of seconds between fetching values from the pump. Default is 30</description>
|
||||||
|
<advanced>true</advanced>
|
||||||
|
<unitLabel>seconds</unitLabel>
|
||||||
|
</parameter>
|
||||||
|
</config-description>
|
||||||
|
</thing-type>
|
||||||
|
|
||||||
</thing:thing-descriptions>
|
</thing:thing-descriptions>
|
||||||
|
|
|
@ -10,16 +10,18 @@
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: EPL-2.0
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
*/
|
*/
|
||||||
package org.openhab.binding.bluetooth.grundfosalpha.internal;
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.handler;
|
||||||
|
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.openhab.binding.bluetooth.grundfosalpha.internal.GrundfosAlphaBindingConstants;
|
||||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||||
import org.openhab.core.library.types.QuantityType;
|
import org.openhab.core.library.types.QuantityType;
|
||||||
import org.openhab.core.library.unit.SIUnits;
|
import org.openhab.core.library.unit.SIUnits;
|
||||||
|
@ -34,23 +36,23 @@ import org.openhab.core.thing.binding.ThingHandlerCallback;
|
||||||
import org.openhab.core.util.HexUtils;
|
import org.openhab.core.util.HexUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the {@link GrundfosAlphaHandler}.
|
* Test the {@link GrundfosAlphaReaderHandler}.
|
||||||
*
|
*
|
||||||
* @author Markus Heberling - Initial contribution
|
* @author Markus Heberling - Initial contribution
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class GrundfosAlphaHandlerTest {
|
class GrundfosAlphaReaderHandlerTest {
|
||||||
|
|
||||||
private @Mock Thing thingMock;
|
private @NonNullByDefault({}) @Mock Thing thingMock;
|
||||||
|
private @NonNullByDefault({}) @Mock ThingHandlerCallback callback;
|
||||||
private @Mock ThingHandlerCallback callback;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testMessageType0xf1() {
|
public void testMessageType0xf1() {
|
||||||
byte[] data = HexUtils.hexToBytes("15f130017a5113030300994109589916613003004005");
|
byte[] data = HexUtils.hexToBytes("15f130017a5113030300994109589916613003004005");
|
||||||
final BluetoothScanNotification scanNotification = new BluetoothScanNotification();
|
final BluetoothScanNotification scanNotification = new BluetoothScanNotification();
|
||||||
scanNotification.setManufacturerData(data);
|
scanNotification.setManufacturerData(data);
|
||||||
final GrundfosAlphaHandler handler = new GrundfosAlphaHandler(thingMock);
|
final GrundfosAlphaReaderHandler handler = new GrundfosAlphaReaderHandler(thingMock);
|
||||||
handler.setCallback(callback);
|
handler.setCallback(callback);
|
||||||
handler.onScanRecordReceived(scanNotification);
|
handler.onScanRecordReceived(scanNotification);
|
||||||
|
|
||||||
|
@ -67,23 +69,23 @@ class GrundfosAlphaHandlerTest {
|
||||||
byte[] data = HexUtils.hexToBytes("14f23001650305065419b9180f011f007c1878170d");
|
byte[] data = HexUtils.hexToBytes("14f23001650305065419b9180f011f007c1878170d");
|
||||||
final BluetoothScanNotification scanNotification = new BluetoothScanNotification();
|
final BluetoothScanNotification scanNotification = new BluetoothScanNotification();
|
||||||
scanNotification.setManufacturerData(data);
|
scanNotification.setManufacturerData(data);
|
||||||
final GrundfosAlphaHandler handler = new GrundfosAlphaHandler(thingMock);
|
final GrundfosAlphaReaderHandler handler = new GrundfosAlphaReaderHandler(thingMock);
|
||||||
handler.setCallback(callback);
|
handler.setCallback(callback);
|
||||||
handler.onScanRecordReceived(scanNotification);
|
handler.onScanRecordReceived(scanNotification);
|
||||||
|
|
||||||
verify(callback).statusUpdated(thingMock,
|
verify(callback).statusUpdated(thingMock,
|
||||||
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
|
new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null));
|
||||||
verify(callback).stateUpdated(
|
verify(callback).stateUpdated(
|
||||||
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_TYPE_BATTERY_LEVEL),
|
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_BATTERY_LEVEL),
|
||||||
new QuantityType<>(75, Units.PERCENT));
|
new QuantityType<>(75, Units.PERCENT));
|
||||||
verify(callback).stateUpdated(
|
verify(callback).stateUpdated(
|
||||||
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_TYPE_FLOW_RATE),
|
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_FLOW_RATE),
|
||||||
new QuantityType<>(0.98939496, Units.CUBICMETRE_PER_HOUR));
|
new QuantityType<>(0.98939496, Units.CUBICMETRE_PER_HOUR));
|
||||||
verify(callback).stateUpdated(
|
verify(callback).stateUpdated(
|
||||||
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_TYPE_PUMP_HEAD),
|
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_PUMP_HEAD),
|
||||||
new QuantityType<>(1.9315165, SIUnits.METRE));
|
new QuantityType<>(1.9315165, SIUnits.METRE));
|
||||||
verify(callback).stateUpdated(
|
verify(callback).stateUpdated(
|
||||||
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_TYPE_PUMP_TEMPERATUR),
|
new ChannelUID(thingMock.getUID(), GrundfosAlphaBindingConstants.CHANNEL_PUMP_TEMPERATURE),
|
||||||
new QuantityType<>(31, SIUnits.CELSIUS));
|
new QuantityType<>(31, SIUnits.CELSIUS));
|
||||||
|
|
||||||
verifyNoMoreInteractions(callback);
|
verifyNoMoreInteractions(callback);
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.core.util.HexUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link CRC16Calculator}.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class CRC16CalculatorTest {
|
||||||
|
|
||||||
|
private static final int MAX_16BIT = 0xFFFF;
|
||||||
|
private static final int MSB_16BIT = 0x8000;
|
||||||
|
private static final int POLYNOMIAL = 0x1021; // CRC-16-CCITT polynomial
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void precomputedValuesAreCorrect() {
|
||||||
|
int[] table = new int[256];
|
||||||
|
for (int byteValue = 0; byteValue < 256; byteValue++) {
|
||||||
|
table[byteValue] = computeCRCForByte(byteValue);
|
||||||
|
}
|
||||||
|
assertThat(table, is(CRC16Calculator.LOOKUP_TABLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int computeCRCForByte(int inputByte) {
|
||||||
|
int shiftedInput = inputByte << 8;
|
||||||
|
int crc = 0;
|
||||||
|
|
||||||
|
for (int bit = 0; bit < 8; bit++) {
|
||||||
|
if (((shiftedInput ^ crc) & MSB_16BIT) != 0) {
|
||||||
|
crc = (crc << 1) ^ POLYNOMIAL;
|
||||||
|
} else {
|
||||||
|
crc <<= 1;
|
||||||
|
}
|
||||||
|
shiftedInput <<= 1;
|
||||||
|
crc &= MAX_16BIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkIsTrueForValidResponse() {
|
||||||
|
byte[] response = HexUtils.hexToBytes(
|
||||||
|
"2430F8E70A2C000100010000254357878B439781803D21B00040F19C0040EA4A404536FDB4FFC00000421C000042040000017317");
|
||||||
|
assertThat(CRC16Calculator.check(response), is(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkIsFalseForInvalidResponse() {
|
||||||
|
byte[] response = HexUtils.hexToBytes(
|
||||||
|
"2430F8E70A2C000100010000254357878B439781803D21B00040F19C0040EA4A404536FDB4FFC00000421C000042040000017318");
|
||||||
|
assertThat(CRC16Calculator.check(response), is(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkThrowsWhenResponseTooShort() {
|
||||||
|
byte[] response = HexUtils.hexToBytes("2430");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
CRC16Calculator.check(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void checkThrowsWhenInvalidDataLength() {
|
||||||
|
byte[] response = HexUtils.hexToBytes(
|
||||||
|
"2430F8E70A2C000100010000254357878B439781803D21B00040F19C0040EA4A404536FDB4FFC00000421C0000420400000173");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> {
|
||||||
|
CRC16Calculator.check(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.core.util.HexUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link MessageType}.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class MessageTypeTest {
|
||||||
|
@Test
|
||||||
|
void requestFlowHead() {
|
||||||
|
String expected = "27 07 E7 F8 0A 03 5D 01 21 52 1F";
|
||||||
|
assertThat(HexUtils.bytesToHex(MessageType.FlowHead.request(), " "), is(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requestPower() {
|
||||||
|
String expected = "27 07 E7 F8 0A 03 57 00 45 8A CD";
|
||||||
|
assertThat(HexUtils.bytesToHex(MessageType.Power.request(), " "), is(expected));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2025 Contributors to the openHAB project
|
||||||
|
*
|
||||||
|
* See the NOTICE file(s) distributed with this work for additional
|
||||||
|
* information.
|
||||||
|
*
|
||||||
|
* This program and the accompanying materials are made available under the
|
||||||
|
* terms of the Eclipse Public License 2.0 which is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-2.0
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: EPL-2.0
|
||||||
|
*/
|
||||||
|
package org.openhab.binding.bluetooth.grundfosalpha.internal.protocol;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.*;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.openhab.core.util.HexUtils;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.Level;
|
||||||
|
import ch.qos.logback.classic.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ResponseMessage}.
|
||||||
|
*
|
||||||
|
* @author Jacob Laursen - Initial contribution
|
||||||
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
|
public class ResponseMessageTest {
|
||||||
|
@BeforeEach
|
||||||
|
public void setUp() {
|
||||||
|
final Logger logger = (Logger) LoggerFactory.getLogger(ResponseMessage.class);
|
||||||
|
logger.setLevel(Level.OFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addPacketFullFlowRateResponseIsParsedWhenValid() {
|
||||||
|
byte[] packet1 = HexUtils.hexToBytes("2423F8E70A1F000130010000183952A66C468F48");
|
||||||
|
byte[] packet2 = HexUtils.hexToBytes("AC7FFFFFFF7FFFFFFF41FF21397FFFFFFF44A8");
|
||||||
|
var decoder = new ResponseMessage();
|
||||||
|
boolean isFull;
|
||||||
|
isFull = decoder.addPacket(packet1);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet2);
|
||||||
|
assertThat(isFull, is(true));
|
||||||
|
if (isFull) {
|
||||||
|
Map<SensorDataType, BigDecimal> values = decoder.decode();
|
||||||
|
assertThat(values.entrySet(), hasSize(2));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.Flow, new BigDecimal("0.723")));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.Head, new BigDecimal("1.83403")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addPacketFullPowerResponseIsParsedWhenValid() {
|
||||||
|
byte[] packet1 = HexUtils.hexToBytes("2430F8E70A2C000100010000254357878B439781");
|
||||||
|
byte[] packet2 = HexUtils.hexToBytes("803D21B00040F19C0040EA4A404536FDB4FFC000");
|
||||||
|
byte[] packet3 = HexUtils.hexToBytes("00421C000042040000017317");
|
||||||
|
var decoder = new ResponseMessage();
|
||||||
|
boolean isFull;
|
||||||
|
isFull = decoder.addPacket(packet1);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet2);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet3);
|
||||||
|
assertThat(isFull, is(true));
|
||||||
|
if (isFull) {
|
||||||
|
Map<SensorDataType, BigDecimal> values = decoder.decode();
|
||||||
|
assertThat(values.entrySet(), hasSize(3));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.VoltageAC, new BigDecimal("215.5")));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.PowerConsumption, new BigDecimal("7.6")));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.MotorSpeed, new BigDecimal("2928")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addPacketFullPowerResponseWhileAwaitingContinuationIsParsedWhenValid() {
|
||||||
|
byte[] packet1 = HexUtils.hexToBytes("2430F8E70A2C000100010000254357878B439781");
|
||||||
|
byte[] packet2 = HexUtils.hexToBytes("2430F8E70A2C000100010000254357878B439781");
|
||||||
|
byte[] packet3 = HexUtils.hexToBytes("803D21B00040F19C0040EA4A404536FDB4FFC000");
|
||||||
|
byte[] packet4 = HexUtils.hexToBytes("00421C000042040000017317");
|
||||||
|
var decoder = new ResponseMessage();
|
||||||
|
boolean isFull;
|
||||||
|
isFull = decoder.addPacket(packet1);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet2);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet3);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet4);
|
||||||
|
assertThat(isFull, is(true));
|
||||||
|
if (isFull) {
|
||||||
|
Map<SensorDataType, BigDecimal> values = decoder.decode();
|
||||||
|
assertThat(values.entrySet(), hasSize(3));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.VoltageAC, new BigDecimal("215.5")));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.PowerConsumption, new BigDecimal("7.6")));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.MotorSpeed, new BigDecimal("2928")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addPacketFullPowerResponseAfterOutOfSyncPacketIsParsedWhenValid() {
|
||||||
|
byte[] packet1 = HexUtils.hexToBytes("AC7FFFFFFF7FFFFFFF41FF21397FFFFFFF44A8");
|
||||||
|
byte[] packet2 = HexUtils.hexToBytes("2430F8E70A2C000100010000254357878B439781");
|
||||||
|
byte[] packet3 = HexUtils.hexToBytes("803D21B00040F19C0040EA4A404536FDB4FFC000");
|
||||||
|
byte[] packet4 = HexUtils.hexToBytes("00421C000042040000017317");
|
||||||
|
var decoder = new ResponseMessage();
|
||||||
|
boolean isFull;
|
||||||
|
isFull = decoder.addPacket(packet1);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet2);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet3);
|
||||||
|
assertThat(isFull, is(false));
|
||||||
|
isFull = decoder.addPacket(packet4);
|
||||||
|
assertThat(isFull, is(true));
|
||||||
|
if (isFull) {
|
||||||
|
Map<SensorDataType, BigDecimal> values = decoder.decode();
|
||||||
|
assertThat(values.entrySet(), hasSize(3));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.VoltageAC, new BigDecimal("215.5")));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.PowerConsumption, new BigDecimal("7.6")));
|
||||||
|
assertThat(values, hasEntry(SensorDataType.MotorSpeed, new BigDecimal("2928")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue