mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 07:02:02 +01:00
[bluetooth.govee] Govee Bluetooth Binding initial contribution (#8610)
Signed-off-by: Connor Petty <mistercpp2000+gitsignoff@gmail.com>
This commit is contained in:
parent
f5ee685556
commit
239e33af26
@ -32,6 +32,7 @@
|
||||
/bundles/org.openhab.binding.bluetooth.daikinmadoka/ @blafois
|
||||
/bundles/org.openhab.binding.bluetooth.enoceanble/ @pfink
|
||||
/bundles/org.openhab.binding.bluetooth.generic/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.govee/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.roaming/ @cpmeister
|
||||
/bundles/org.openhab.binding.bluetooth.ruuvitag/ @ssalonen
|
||||
/bundles/org.openhab.binding.boschindego/ @jofleck
|
||||
|
@ -146,6 +146,11 @@
|
||||
<artifactId>org.openhab.binding.bluetooth.generic</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth.govee</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth.roaming</artifactId>
|
||||
|
13
bundles/org.openhab.binding.bluetooth.govee/NOTICE
Normal file
13
bundles/org.openhab.binding.bluetooth.govee/NOTICE
Normal file
@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
68
bundles/org.openhab.binding.bluetooth.govee/README.md
Normal file
68
bundles/org.openhab.binding.bluetooth.govee/README.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Govee
|
||||
|
||||
This extension adds support for [Govee](https://www.govee.com/) Bluetooth Devices.
|
||||
|
||||
## Supported Things
|
||||
|
||||
Only two thing types are supported by this extension at the moment.
|
||||
|
||||
| Thing Type ID | Description | Supported Models |
|
||||
|------------------------|-------------------------------------------|-------------------------------------------------------------|
|
||||
| goveeHygrometer | Govee Thermo-Hygrometer | H5051,H5071 |
|
||||
| goveeHygrometerMonitor | Govee Thermo-Hygrometer w/ Warning Alarms | H5052,H5072,H5074,H5075,H5101,H5102,H5177,H5179,B5175,B5178 |
|
||||
|
||||
## Discovery
|
||||
|
||||
As any other Bluetooth device, Govee devices are discovered automatically by the corresponding bridge.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
Govee things have the following configuration parameters:
|
||||
|
||||
| Thing | Parameter | Required | Default | Description |
|
||||
|-----------------------------|-------------------------|----------|---------|-----------------------------------------------------------------------------------|
|
||||
| all | address | yes | | The Bluetooth address of the device (in format "XX:XX:XX:XX:XX:XX") |
|
||||
| all | refreshInterval | | 300 | How often, in seconds, the sensor data of the device should be refreshed |
|
||||
| goveeHygrometer<sup>1</sup> | temperatureCalibration | no | | Offset to apply to temperature<sup>2</sup> sensor readings |
|
||||
| goveeHygrometer<sup>1</sup> | humidityCalibration | no | | Offset to apply to humidity sensor readings |
|
||||
| goveeHygrometerMonitor | temperatureWarningAlarm | | false | Enables warning alarms to be broadcast when temperature is out of specified range |
|
||||
| goveeHygrometerMonitor | temperatureWarningMin | | 0 | The lower safe temperature<sup>2</sup> threshold <sup>3</sup> |
|
||||
| goveeHygrometerMonitor | temperatureWarningMax | | 0 | The upper safe temperature<sup>2</sup> threshold <sup>3</sup> |
|
||||
| goveeHygrometerMonitor | humidityWarningAlarm | | false | Enables warning alarms to be broadcast when humidity is out of specified range |
|
||||
| goveeHygrometerMonitor | humidityWarningMin | | 0 | The lower safe humidity threshold <sup>3</sup> |
|
||||
| goveeHygrometerMonitor | humidityWarningMax | | 0 | The upper safe humidity threshold <sup>3</sup> |
|
||||
|
||||
1. Available to both `goveeHygrometer` and `goveeHygrometerMonitor` thing types.
|
||||
2. In °C
|
||||
3. Only applies if alarm feature is enabled
|
||||
|
||||
## Channels
|
||||
|
||||
Govee things have the following channels in addition to the default bluetooth channels:
|
||||
|
||||
| Thing | Channel ID | Item Type | Description |
|
||||
|-----------------------------|------------------|------------------------|----------------------------------------------------------------|
|
||||
| goveeHygrometer<sup>1</sup> | temperature | Number:Temperature | The measured temperature |
|
||||
| goveeHygrometer<sup>1</sup> | humidity | Number:Dimensionless | The measured relative humidity |
|
||||
| goveeHygrometer<sup>1</sup> | battery | Number:Dimensionless | The measured battery percentage |
|
||||
| goveeHygrometerMonitor | temperatureAlarm | Switch | Indicates if current temperature is out of range. <sup>2</sup> |
|
||||
| goveeHygrometerMonitor | humidityAlarm | Switch | Indicates if current humidity is out of range. <sup>2</sup> |
|
||||
|
||||
1. Available to both `goveeHygrometer` and `goveeHygrometerMonitor` thing types.
|
||||
2. Only applies if warning alarms are enabled in the configuration.
|
||||
|
||||
## Example
|
||||
|
||||
demo.things:
|
||||
|
||||
```
|
||||
bluetooth:goveeHygrometer:hci0:beacon "Govee Temperature Humidity Monitor" (bluetooth:bluez:hci0) [ address="12:34:56:78:9A:BC" ]
|
||||
```
|
||||
|
||||
demo.items:
|
||||
|
||||
```
|
||||
Number:Temperature temperature "Room Temperature [%.1f %unit%]" { channel="bluetooth:goveeHygrometer:hci0:beacon:temperature" }
|
||||
Number:Dimensionless humidity "Humidity [%.0f %unit%]" { channel="bluetooth:goveeHygrometer:hci0:beacon:humidity" }
|
||||
Number:Dimensionless battery "Battery [%.0f %unit%]" { channel="bluetooth:goveeHygrometer:hci0:beacon:battery" }
|
||||
```
|
34
bundles/org.openhab.binding.bluetooth.govee/pom.xml
Normal file
34
bundles/org.openhab.binding.bluetooth.govee/pom.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.bluetooth.govee</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Govee Bluetooth Adapter</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.binding.bluetooth</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<type>test-jar</type>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</project>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.bluetooth.govee-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-bluetooth-govee" description="Bluetooth Binding Govee" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.govee/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface GattMessage {
|
||||
|
||||
public byte[] getPayload();
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class GattSocket<T extends GattMessage, R extends GattMessage> {
|
||||
|
||||
private static final Future<?> COMPLETED_FUTURE = CompletableFuture.completedFuture(null);
|
||||
|
||||
private final Deque<MessageProcessor> messageProcessors = new ConcurrentLinkedDeque<>();
|
||||
|
||||
public void registerMessageHandler(MessageHandler<T, R> messageHandler) {
|
||||
// we need to use a dummy future since ConcurrentHashMap doesn't allow null values
|
||||
messageProcessors.addFirst(new MessageProcessor(messageHandler, COMPLETED_FUTURE));
|
||||
}
|
||||
|
||||
protected abstract ScheduledExecutorService getScheduler();
|
||||
|
||||
public void sendMessage(MessageServicer<T, R> messageServicer) {
|
||||
T message = messageServicer.createMessage();
|
||||
|
||||
CompletableFuture<@Nullable Void> messageFuture = sendMessage(message);
|
||||
|
||||
Future<?> timeoutFuture = getScheduler().schedule(() -> {
|
||||
messageFuture.completeExceptionally(new TimeoutException("Timeout while waiting for response"));
|
||||
}, messageServicer.getTimeout(TimeUnit.NANOSECONDS), TimeUnit.NANOSECONDS);
|
||||
|
||||
MessageProcessor processor = new MessageProcessor(messageServicer, timeoutFuture);
|
||||
messageProcessors.addLast(processor);
|
||||
|
||||
messageFuture.whenComplete((v, ex) -> {
|
||||
if (ex instanceof CompletionException) {
|
||||
ex = ex.getCause();
|
||||
}
|
||||
if (ex != null) {
|
||||
if (messageServicer.handleFailedMessage(message, ex)) {
|
||||
timeoutFuture.cancel(false);
|
||||
messageProcessors.remove(processor);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> sendMessage(T message) {
|
||||
List<byte[]> packets = createPackets(message);
|
||||
var futures = packets.stream()//
|
||||
.map(this::sendPacket)//
|
||||
.toArray(CompletableFuture[]::new);
|
||||
|
||||
return CompletableFuture.allOf(futures);
|
||||
}
|
||||
|
||||
protected List<byte[]> createPackets(T message) {
|
||||
return List.of(message.getPayload());
|
||||
}
|
||||
|
||||
protected abstract void parsePacket(byte[] packet, Consumer<R> messageHandler);
|
||||
|
||||
protected abstract CompletableFuture<@Nullable Void> sendPacket(byte[] value);
|
||||
|
||||
public void receivePacket(byte[] packet) {
|
||||
parsePacket(packet, this::handleMessage);
|
||||
}
|
||||
|
||||
private void handleMessage(R message) {
|
||||
for (Iterator<MessageProcessor> it = messageProcessors.iterator(); it.hasNext();) {
|
||||
MessageProcessor processor = it.next();
|
||||
if (processor.messageHandler.handleReceivedMessage(message)) {
|
||||
processor.timeoutFuture.cancel(false);
|
||||
it.remove();
|
||||
// we want to return after the first message servicer handles the message
|
||||
if (processor.timeoutFuture != COMPLETED_FUTURE) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageProcessor {
|
||||
private MessageHandler<T, R> messageHandler;
|
||||
private Future<?> timeoutFuture;
|
||||
|
||||
public MessageProcessor(MessageHandler<T, R> messageHandler, Future<?> timeoutFuture) {
|
||||
this.messageHandler = messageHandler;
|
||||
this.timeoutFuture = timeoutFuture;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MessageHandler<T extends GattMessage, R extends GattMessage> {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param payload
|
||||
* @return true if this handler should be removed from the handler list
|
||||
*/
|
||||
public boolean handleReceivedMessage(R message);
|
||||
|
||||
/**
|
||||
*
|
||||
* @param payload
|
||||
* @return true if this handler should be removed from the handler list
|
||||
*/
|
||||
public boolean handleFailedMessage(T message, Throwable th);
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface MessageServicer<T extends GattMessage, R extends GattMessage>
|
||||
extends MessageHandler<T, R>, MessageSupplier<T> {
|
||||
|
||||
public long getTimeout(TimeUnit unit);
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
public interface MessageSupplier<M extends GattMessage> {
|
||||
|
||||
public M createMessage();
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class SimpleGattSocket<M extends GattMessage> extends GattSocket<M, M> {
|
||||
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SimpleMessage implements GattMessage {
|
||||
|
||||
private byte[] data;
|
||||
|
||||
public SimpleMessage(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPayload() {
|
||||
return data;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SimpleMessageHandler<M extends GattMessage> extends MessageHandler<M, M> {
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.gattserial;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface SimpleMessageServicer<M extends GattMessage> extends MessageServicer<M, M> {
|
||||
|
||||
}
|
@ -0,0 +1,472 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
|
||||
import org.openhab.binding.bluetooth.BluetoothDescriptor;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.BluetoothService;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
|
||||
import org.openhab.core.common.NamedThreadFactory;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This is a base implementation for more specific thing handlers that require constant connection to bluetooth devices.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution and API
|
||||
* @deprecated once CompletableFutures are supported in the actual ConnectedBluetoothHandler, this class can be deleted
|
||||
*/
|
||||
@Deprecated
|
||||
@NonNullByDefault
|
||||
public class ConnectedBluetoothHandler extends BeaconBluetoothHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ConnectedBluetoothHandler.class);
|
||||
|
||||
private final Condition connectionCondition = deviceLock.newCondition();
|
||||
private final Condition serviceDiscoveryCondition = deviceLock.newCondition();
|
||||
private final Condition charCompleteCondition = deviceLock.newCondition();
|
||||
|
||||
private @Nullable Future<?> reconnectJob;
|
||||
private @Nullable Future<?> pendingDisconnect;
|
||||
private @Nullable BluetoothCharacteristic ongoingCharacteristic;
|
||||
private @Nullable BluetoothCompletionStatus completeStatus;
|
||||
|
||||
private boolean connectOnDemand;
|
||||
private int idleDisconnectDelayMs = 1000;
|
||||
|
||||
protected @Nullable ScheduledExecutorService connectionTaskExecutor;
|
||||
private volatile boolean servicesDiscovered;
|
||||
|
||||
public ConnectedBluetoothHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
|
||||
// super.initialize adds callbacks that might require the connectionTaskExecutor to be present, so we initialize
|
||||
// the connectionTaskExecutor first
|
||||
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1,
|
||||
new NamedThreadFactory("bluetooth-connection-" + thing.getThingTypeUID(), true));
|
||||
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
executor.setRemoveOnCancelPolicy(true);
|
||||
connectionTaskExecutor = executor;
|
||||
|
||||
super.initialize();
|
||||
|
||||
connectOnDemand = true;
|
||||
|
||||
Object idleDisconnectDelayRaw = getConfig().get("idleDisconnectDelay");
|
||||
idleDisconnectDelayMs = 1000;
|
||||
if (idleDisconnectDelayRaw instanceof Number) {
|
||||
idleDisconnectDelayMs = ((Number) idleDisconnectDelayRaw).intValue();
|
||||
}
|
||||
|
||||
if (!connectOnDemand) {
|
||||
reconnectJob = executor.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
device.connect();
|
||||
// we do not set the Thing status here, because we will anyhow receive a call to
|
||||
// onConnectionStateChange
|
||||
} else {
|
||||
// just in case it was already connected to begin with
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (!servicesDiscovered && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("Unexpected error occurred", ex);
|
||||
}
|
||||
}, 0, 30, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
cancel(reconnectJob);
|
||||
reconnectJob = null;
|
||||
cancel(pendingDisconnect);
|
||||
pendingDisconnect = null;
|
||||
|
||||
super.dispose();
|
||||
|
||||
shutdown(connectionTaskExecutor);
|
||||
connectionTaskExecutor = null;
|
||||
}
|
||||
|
||||
private static void cancel(@Nullable Future<?> future) {
|
||||
if (future != null) {
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdown(@Nullable ScheduledExecutorService executor) {
|
||||
if (executor != null) {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduledExecutorService getConnectionTaskExecutor() {
|
||||
var executor = connectionTaskExecutor;
|
||||
if (executor == null) {
|
||||
throw new IllegalStateException("characteristicScheduler has not been initialized");
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
|
||||
private void scheduleDisconnect() {
|
||||
cancel(pendingDisconnect);
|
||||
pendingDisconnect = getConnectionTaskExecutor().schedule(device::disconnect, idleDisconnectDelayMs,
|
||||
TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void connectAndWait() throws ConnectionException, TimeoutException, InterruptedException {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
return;
|
||||
}
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTING) {
|
||||
if (!device.connect()) {
|
||||
throw new ConnectionException("Failed to start connecting");
|
||||
}
|
||||
}
|
||||
logger.debug("waiting for connection");
|
||||
if (!awaitConnection(1, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Connection attempt timeout.");
|
||||
}
|
||||
logger.debug("connection successful");
|
||||
if (!servicesDiscovered) {
|
||||
logger.debug("discovering services");
|
||||
device.discoverServices();
|
||||
if (!awaitServiceDiscovery(20, TimeUnit.SECONDS)) {
|
||||
throw new TimeoutException("Service discovery timeout");
|
||||
}
|
||||
logger.debug("service discovery successful");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean awaitConnection(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = connectionCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitCharacteristicComplete(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (ongoingCharacteristic != null) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = charCompleteCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean awaitServiceDiscovery(long timeout, TimeUnit unit) throws InterruptedException {
|
||||
deviceLock.lock();
|
||||
try {
|
||||
long nanosTimeout = unit.toNanos(timeout);
|
||||
while (!servicesDiscovered) {
|
||||
if (nanosTimeout <= 0L) {
|
||||
return false;
|
||||
}
|
||||
nanosTimeout = serviceDiscoveryCondition.awaitNanos(nanosTimeout);
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private BluetoothCharacteristic connectAndGetCharacteristic(UUID serviceUUID, UUID characteristicUUID)
|
||||
throws BluetoothException, TimeoutException, InterruptedException {
|
||||
connectAndWait();
|
||||
BluetoothService service = device.getServices(serviceUUID);
|
||||
if (service == null) {
|
||||
throw new BluetoothException("Service with uuid " + serviceUUID + " could not be found");
|
||||
}
|
||||
BluetoothCharacteristic characteristic = service.getCharacteristic(characteristicUUID);
|
||||
if (characteristic == null) {
|
||||
throw new BluetoothException("Characteristic with uuid " + characteristicUUID + " could not be found");
|
||||
}
|
||||
return characteristic;
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> executeWithConnection(UUID serviceUUID, UUID characteristicUUID,
|
||||
CallableFunction<BluetoothCharacteristic, T> callable) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
var executor = connectionTaskExecutor;
|
||||
if (executor != null) {
|
||||
executor.execute(() -> {
|
||||
cancel(pendingDisconnect);
|
||||
try {
|
||||
BluetoothCharacteristic characteristic = connectAndGetCharacteristic(serviceUUID,
|
||||
characteristicUUID);
|
||||
future.complete(callable.call(characteristic));
|
||||
} catch (InterruptedException e) {
|
||||
future.completeExceptionally(e);
|
||||
return;// we don't want to schedule anything if we receive an interrupt
|
||||
} catch (TimeoutException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
future.completeExceptionally(e);
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
if (connectOnDemand) {
|
||||
scheduleDisconnect();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
future.completeExceptionally(new IllegalStateException("characteristicScheduler has not been initialized"));
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> enableNotifications(UUID serviceUUID, UUID characteristicUUID) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
if (!device.enableNotifications(characteristic)) {
|
||||
throw new BluetoothException(
|
||||
"Failed to start notifications for characteristic: " + characteristic.getUuid());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<@Nullable Void> writeCharacteristic(UUID serviceUUID, UUID characteristicUUID, byte[] data,
|
||||
boolean enableNotification) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
if (enableNotification) {
|
||||
if (!device.enableNotifications(characteristic)) {
|
||||
throw new BluetoothException(
|
||||
"Failed to start characteristic notification" + characteristic.getUuid());
|
||||
}
|
||||
}
|
||||
// now block for completion
|
||||
characteristic.setValue(data);
|
||||
ongoingCharacteristic = characteristic;
|
||||
if (!device.writeCharacteristic(characteristic)) {
|
||||
throw new BluetoothException("Failed to start writing characteristic " + characteristic.getUuid());
|
||||
}
|
||||
if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
|
||||
ongoingCharacteristic = null;
|
||||
throw new TimeoutException(
|
||||
"Timeout waiting for characteristic " + characteristic.getUuid() + " write to finish");
|
||||
}
|
||||
if (completeStatus == BluetoothCompletionStatus.ERROR) {
|
||||
throw new BluetoothException("Failed to write characteristic " + characteristic.getUuid());
|
||||
}
|
||||
logger.debug("Wrote {} to characteristic {} of device {}", HexUtils.bytesToHex(data),
|
||||
characteristic.getUuid(), address);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<byte[]> readCharacteristic(UUID serviceUUID, UUID characteristicUUID) {
|
||||
return executeWithConnection(serviceUUID, characteristicUUID, characteristic -> {
|
||||
// now block for completion
|
||||
ongoingCharacteristic = characteristic;
|
||||
if (!device.readCharacteristic(characteristic)) {
|
||||
throw new BluetoothException("Failed to start reading characteristic " + characteristic.getUuid());
|
||||
}
|
||||
if (!awaitCharacteristicComplete(1, TimeUnit.SECONDS)) {
|
||||
ongoingCharacteristic = null;
|
||||
throw new TimeoutException(
|
||||
"Timeout waiting for characteristic " + characteristic.getUuid() + " read to finish");
|
||||
}
|
||||
if (completeStatus == BluetoothCompletionStatus.ERROR) {
|
||||
throw new BluetoothException("Failed to read characteristic " + characteristic.getUuid());
|
||||
}
|
||||
byte[] data = characteristic.getByteValue();
|
||||
logger.debug("Characteristic {} from {} has been read - value {}", characteristic.getUuid(), address,
|
||||
HexUtils.bytesToHex(data));
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateStatusBasedOnRssi(boolean receivedSignal) {
|
||||
// if there is no signal, we can be sure we are OFFLINE, but if there is a signal, we also have to check whether
|
||||
// we are connected.
|
||||
if (receivedSignal) {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
if (!connectOnDemand) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Device is not connected.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
|
||||
super.onConnectionStateChange(connectionNotification);
|
||||
switch (connectionNotification.getConnectionState()) {
|
||||
case DISCOVERED:
|
||||
// The device is now known on the Bluetooth network, so we can do something...
|
||||
if (!connectOnDemand) {
|
||||
getConnectionTaskExecutor().submit(() -> {
|
||||
if (device.getConnectionState() != ConnectionState.CONNECTED) {
|
||||
if (!device.connect()) {
|
||||
logger.debug("Error connecting to device after discovery.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CONNECTED:
|
||||
deviceLock.lock();
|
||||
try {
|
||||
connectionCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
if (!connectOnDemand) {
|
||||
getConnectionTaskExecutor().submit(() -> {
|
||||
if (!servicesDiscovered && !device.discoverServices()) {
|
||||
logger.debug("Error while discovering services");
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case DISCONNECTED:
|
||||
var future = pendingDisconnect;
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
}
|
||||
if (!connectOnDemand) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicReadComplete(BluetoothCharacteristic characteristic, BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicReadComplete(characteristic, status);
|
||||
deviceLock.lock();
|
||||
try {
|
||||
if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
|
||||
completeStatus = status;
|
||||
ongoingCharacteristic = null;
|
||||
charCompleteCondition.signal();
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
|
||||
BluetoothCompletionStatus status) {
|
||||
super.onCharacteristicWriteComplete(characteristic, status);
|
||||
deviceLock.lock();
|
||||
try {
|
||||
if (ongoingCharacteristic != null && ongoingCharacteristic.getUuid().equals(characteristic.getUuid())) {
|
||||
completeStatus = status;
|
||||
ongoingCharacteristic = null;
|
||||
charCompleteCondition.signal();
|
||||
}
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered() {
|
||||
super.onServicesDiscovered();
|
||||
deviceLock.lock();
|
||||
try {
|
||||
this.servicesDiscovered = true;
|
||||
serviceDiscoveryCondition.signal();
|
||||
} finally {
|
||||
deviceLock.unlock();
|
||||
}
|
||||
logger.debug("Service discovery completed for '{}'", address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Recieved update {} to characteristic {} of device {}",
|
||||
HexUtils.bytesToHex(characteristic.getByteValue()), characteristic.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorUpdate(BluetoothDescriptor descriptor) {
|
||||
super.onDescriptorUpdate(descriptor);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Received update {} to descriptor {} of device {}", HexUtils.bytesToHex(descriptor.getValue()),
|
||||
descriptor.getUuid(), address);
|
||||
}
|
||||
}
|
||||
|
||||
public static class BluetoothException extends Exception {
|
||||
|
||||
public BluetoothException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConnectionException extends BluetoothException {
|
||||
|
||||
public ConnectionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public static interface CallableFunction<U, R> {
|
||||
public R call(U arg) throws Exception;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link GoveeBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GoveeBindingConstants {
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_HYGROMETER = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID,
|
||||
"goveeHygrometer");
|
||||
public static final ThingTypeUID THING_TYPE_HYGROMETER_MONITOR = new ThingTypeUID(
|
||||
BluetoothBindingConstants.BINDING_ID, "goveeHygrometerMonitor");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_HYGROMETER,
|
||||
THING_TYPE_HYGROMETER_MONITOR);
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_ID_BATTERY = "battery";
|
||||
public static final String CHANNEL_ID_TEMPERATURE = "temperature";
|
||||
public static final String CHANNEL_ID_TEMPERATURE_ALARM = "temperatureAlarm";
|
||||
public static final String CHANNEL_ID_HUMIDITY = "humidity";
|
||||
public static final String CHANNEL_ID_HUMIDITY_ALARM = "humidityAlarm";
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import static org.openhab.binding.bluetooth.govee.internal.GoveeBindingConstants.SUPPORTED_THING_TYPES_UIDS;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
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.BluetoothDiscoveryParticipant;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link GoveeDiscoveryParticipant} handles discovery of Govee bluetooth devices
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = BluetoothDiscoveryParticipant.class)
|
||||
public class GoveeDiscoveryParticipant implements BluetoothDiscoveryParticipant {
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return SUPPORTED_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
private ThingUID getThingUID(BluetoothDiscoveryDevice device, ThingTypeUID thingTypeUID) {
|
||||
return new ThingUID(thingTypeUID, device.getAdapter().getUID(),
|
||||
device.getAddress().toString().toLowerCase().replace(":", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(BluetoothDiscoveryDevice device) {
|
||||
GoveeModel model = GoveeModel.getGoveeModel(device);
|
||||
if (model != null) {
|
||||
return getThingUID(device, model.getThingTypeUID());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(BluetoothDiscoveryDevice device) {
|
||||
GoveeModel model = GoveeModel.getGoveeModel(device);
|
||||
if (model != null) {
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(BluetoothBindingConstants.CONFIGURATION_ADDRESS, device.getAddress().toString());
|
||||
properties.put(Thing.PROPERTY_VENDOR, "Govee");
|
||||
properties.put(Thing.PROPERTY_MODEL_ID, model.name());
|
||||
Integer txPower = device.getTxPower();
|
||||
if (txPower != null) {
|
||||
properties.put(BluetoothBindingConstants.PROPERTY_TXPOWER, Integer.toString(txPower));
|
||||
}
|
||||
|
||||
// Create the discovery result and add to the inbox
|
||||
return DiscoveryResultBuilder.create(getThingUID(device, model.getThingTypeUID()))
|
||||
.withProperties(properties)
|
||||
.withRepresentationProperty(BluetoothBindingConstants.CONFIGURATION_ADDRESS)
|
||||
.withBridge(device.getAdapter().getUID()).withLabel(model.getLabel()).build();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import static org.openhab.binding.bluetooth.govee.internal.GoveeBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link GoveeHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Connor Petty - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.bluetooth.govee", service = ThingHandlerFactory.class)
|
||||
public class GoveeHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_HYGROMETER.equals(thingTypeUID) || THING_TYPE_HYGROMETER_MONITOR.equals(thingTypeUID)) {
|
||||
return new GoveeHygrometerHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.WarningSettingsDTO;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GoveeHygrometerConfiguration {
|
||||
public int refreshInterval = 300;
|
||||
|
||||
public @Nullable Double temperatureCalibration;
|
||||
public boolean temperatureWarningAlarm = false;
|
||||
public double temperatureWarningMin;
|
||||
public double temperatureWarningMax;
|
||||
|
||||
public @Nullable Double humidityCalibration;
|
||||
public boolean humidityWarningAlarm = false;
|
||||
public double humidityWarningMin;
|
||||
public double humidityWarningMax;
|
||||
|
||||
public @Nullable QuantityType<Temperature> getTemperatureCalibration() {
|
||||
var temCali = temperatureCalibration;
|
||||
if (temCali != null) {
|
||||
return new QuantityType<>(temCali, SIUnits.CELSIUS);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable QuantityType<Dimensionless> getHumidityCalibration() {
|
||||
var humCali = humidityCalibration;
|
||||
if (humCali != null) {
|
||||
return new QuantityType<>(humCali, Units.PERCENT);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public WarningSettingsDTO<Temperature> getTemperatureWarningSettings() {
|
||||
WarningSettingsDTO<Temperature> temWarnSettings = new WarningSettingsDTO<>();
|
||||
temWarnSettings.enableAlarm = OnOffType.from(temperatureWarningAlarm);
|
||||
temWarnSettings.min = new QuantityType<>(temperatureWarningMin, SIUnits.CELSIUS);
|
||||
temWarnSettings.max = new QuantityType<>(temperatureWarningMax, SIUnits.CELSIUS);
|
||||
return temWarnSettings;
|
||||
}
|
||||
|
||||
public WarningSettingsDTO<Dimensionless> getHumidityWarningSettings() {
|
||||
WarningSettingsDTO<Dimensionless> humWarnSettings = new WarningSettingsDTO<>();
|
||||
humWarnSettings.enableAlarm = OnOffType.from(humidityWarningAlarm);
|
||||
humWarnSettings.min = new QuantityType<>(humidityWarningMin, Units.PERCENT);
|
||||
humWarnSettings.max = new QuantityType<>(humidityWarningMax, Units.PERCENT);
|
||||
return humWarnSettings;
|
||||
}
|
||||
}
|
@ -0,0 +1,424 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import static org.openhab.binding.bluetooth.govee.internal.GoveeBindingConstants.*;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
|
||||
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
|
||||
import org.openhab.binding.bluetooth.gattserial.MessageServicer;
|
||||
import org.openhab.binding.bluetooth.gattserial.SimpleGattSocket;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetBatteryCommand;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetOrSetHumCaliCommand;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetOrSetHumWarningCommand;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetOrSetTemCaliCommand;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetOrSetTemWarningCommand;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GetTemHumCommand;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.GoveeMessage;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.TemHumDTO;
|
||||
import org.openhab.binding.bluetooth.govee.internal.command.hygrometer.WarningSettingsDTO;
|
||||
import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
|
||||
import org.openhab.binding.bluetooth.util.HeritableFuture;
|
||||
import org.openhab.binding.bluetooth.util.RetryException;
|
||||
import org.openhab.binding.bluetooth.util.RetryFuture;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
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.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GoveeHygrometerHandler extends ConnectedBluetoothHandler {
|
||||
|
||||
private static final UUID SERVICE_UUID = UUID.fromString("494e5445-4c4c-495f-524f-434b535f4857");
|
||||
private static final UUID PROTOCOL_CHAR_UUID = UUID.fromString("494e5445-4c4c-495f-524f-434b535f2011");
|
||||
private static final UUID KEEP_ALIVE_CHAR_UUID = UUID.fromString("494e5445-4c4c-495f-524f-434b535f2012");
|
||||
|
||||
private static final byte[] SCAN_HEADER = { (byte) 0xFF, (byte) 0x88, (byte) 0xEC };
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(GoveeHygrometerHandler.class);
|
||||
|
||||
private final CommandSocket commandSocket = new CommandSocket();
|
||||
|
||||
private GoveeHygrometerConfiguration config = new GoveeHygrometerConfiguration();
|
||||
private GoveeModel model = GoveeModel.H5074;// we use this as our default model
|
||||
|
||||
private CompletableFuture<?> initializeJob = CompletableFuture.completedFuture(null);// initially set to a dummy
|
||||
// future
|
||||
private Future<?> scanJob = CompletableFuture.completedFuture(null);
|
||||
private Future<?> keepAliveJob = CompletableFuture.completedFuture(null);
|
||||
|
||||
public GoveeHygrometerHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
super.initialize();
|
||||
config = getConfigAs(GoveeHygrometerConfiguration.class);
|
||||
|
||||
Map<String, String> properties = thing.getProperties();
|
||||
String modelProp = properties.get(Thing.PROPERTY_MODEL_ID);
|
||||
model = GoveeModel.H5074;
|
||||
if (modelProp != null) {
|
||||
try {
|
||||
model = GoveeModel.valueOf(modelProp);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Initializing Govee Hygrometer {} model: {}", address, model);
|
||||
initializeJob = RetryFuture.composeWithRetry(this::createInitSettingsJob, scheduler)//
|
||||
.thenRun(() -> {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
});
|
||||
scanJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (initializeJob.isDone() && !initializeJob.isCompletedExceptionally()) {
|
||||
logger.debug("refreshing temperature, humidity, and battery");
|
||||
refreshBattery().join();
|
||||
refreshTemperatureAndHumidity().join();
|
||||
connectionTaskExecutor.execute(device::disconnect);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("unable to refresh", ex);
|
||||
}
|
||||
}, 0, config.refreshInterval, TimeUnit.SECONDS);
|
||||
keepAliveJob = connectionTaskExecutor.scheduleWithFixedDelay(() -> {
|
||||
if (device.getConnectionState() == ConnectionState.CONNECTED) {
|
||||
try {
|
||||
GoveeMessage message = new GoveeMessage((byte) 0xAA, (byte) 1, null);
|
||||
writeCharacteristic(SERVICE_UUID, KEEP_ALIVE_CHAR_UUID, message.getPayload(), false);
|
||||
} catch (RuntimeException ex) {
|
||||
logger.warn("unable to send keep alive", ex);
|
||||
}
|
||||
}
|
||||
}, 1, 2, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
initializeJob.cancel(false);
|
||||
scanJob.cancel(false);
|
||||
keepAliveJob.cancel(false);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private CompletableFuture<@Nullable ?> createInitSettingsJob() {
|
||||
|
||||
logger.debug("Initializing Govee Hygrometer {} settings", address);
|
||||
|
||||
QuantityType<Temperature> temCali = config.getTemperatureCalibration();
|
||||
QuantityType<Dimensionless> humCali = config.getHumidityCalibration();
|
||||
WarningSettingsDTO<Temperature> temWarnSettings = config.getTemperatureWarningSettings();
|
||||
WarningSettingsDTO<Dimensionless> humWarnSettings = config.getHumidityWarningSettings();
|
||||
|
||||
final CompletableFuture<@Nullable ?> parent = new HeritableFuture<>();
|
||||
CompletableFuture<@Nullable ?> future = parent;
|
||||
future.complete(null);
|
||||
|
||||
if (temCali != null) {
|
||||
future = future.thenCompose(v -> {
|
||||
CompletableFuture<@Nullable QuantityType<Temperature>> caliFuture = parent.newIncompleteFuture();
|
||||
commandSocket.sendMessage(new GetOrSetTemCaliCommand(temCali, caliFuture));
|
||||
return caliFuture;
|
||||
});
|
||||
}
|
||||
if (humCali != null) {
|
||||
future = future.thenCompose(v -> {
|
||||
CompletableFuture<@Nullable QuantityType<Dimensionless>> caliFuture = parent.newIncompleteFuture();
|
||||
commandSocket.sendMessage(new GetOrSetHumCaliCommand(humCali, caliFuture));
|
||||
return caliFuture;
|
||||
});
|
||||
}
|
||||
if (model.supportsWarningBroadcast()) {
|
||||
future = future.thenCompose(v -> {
|
||||
CompletableFuture<@Nullable WarningSettingsDTO<Temperature>> temWarnFuture = parent
|
||||
.newIncompleteFuture();
|
||||
commandSocket.sendMessage(new GetOrSetTemWarningCommand(temWarnSettings, temWarnFuture));
|
||||
return temWarnFuture;
|
||||
}).thenCompose(v -> {
|
||||
CompletableFuture<@Nullable WarningSettingsDTO<Dimensionless>> humWarnFuture = parent
|
||||
.newIncompleteFuture();
|
||||
commandSocket.sendMessage(new GetOrSetHumWarningCommand(humWarnSettings, humWarnFuture));
|
||||
return humWarnFuture;
|
||||
});
|
||||
}
|
||||
|
||||
// CompletableFuture.exceptionallyCompose isn't available yet so we have to compose it manually for now.
|
||||
CompletableFuture<@Nullable Void> retFuture = future.newIncompleteFuture();
|
||||
future.whenComplete((v, th) -> {
|
||||
if (th instanceof CompletionException) {
|
||||
th = th.getCause();
|
||||
}
|
||||
if (th instanceof RuntimeException) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"Failed to initialize device: " + th.getMessage());
|
||||
retFuture.completeExceptionally(th);
|
||||
} else if (th != null) {
|
||||
logger.debug("Failure to initialize device: {}. Retrying in 30 seconds", th.getMessage());
|
||||
retFuture.completeExceptionally(new RetryException(30, TimeUnit.SECONDS));
|
||||
} else {
|
||||
retFuture.complete(null);
|
||||
}
|
||||
});
|
||||
return retFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
super.handleCommand(channelUID, command);
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ID_BATTERY:
|
||||
if (command == RefreshType.REFRESH) {
|
||||
refreshBattery();
|
||||
}
|
||||
return;
|
||||
case CHANNEL_ID_TEMPERATURE:
|
||||
case CHANNEL_ID_HUMIDITY:
|
||||
if (command == RefreshType.REFRESH) {
|
||||
refreshTemperatureAndHumidity();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<@Nullable ?> refreshBattery() {
|
||||
CompletableFuture<@Nullable QuantityType<Dimensionless>> future = new CompletableFuture<>();
|
||||
commandSocket.sendMessage(new GetBatteryCommand(future));
|
||||
future.whenCompleteAsync(this::updateBattery, scheduler);
|
||||
return future;
|
||||
}
|
||||
|
||||
private void updateBattery(@Nullable QuantityType<Dimensionless> result, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
logger.debug("Failed to get battery: {}", th.getMessage());
|
||||
}
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
updateState(CHANNEL_ID_BATTERY, result);
|
||||
}
|
||||
|
||||
private CompletableFuture<@Nullable ?> refreshTemperatureAndHumidity() {
|
||||
CompletableFuture<@Nullable TemHumDTO> future = new CompletableFuture<>();
|
||||
commandSocket.sendMessage(new GetTemHumCommand(future));
|
||||
future.whenCompleteAsync(this::updateTemperatureAndHumidity, scheduler);
|
||||
return future;
|
||||
}
|
||||
|
||||
private void updateTemperatureAndHumidity(@Nullable TemHumDTO result, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
logger.debug("Failed to get temperature/humidity: {}", th.getMessage());
|
||||
}
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
QuantityType<Temperature> tem = result.temperature;
|
||||
QuantityType<Dimensionless> hum = result.humidity;
|
||||
if (tem == null || hum == null) {
|
||||
return;
|
||||
}
|
||||
updateState(CHANNEL_ID_TEMPERATURE, tem);
|
||||
updateState(CHANNEL_ID_HUMIDITY, hum);
|
||||
if (model.supportsWarningBroadcast()) {
|
||||
updateAlarm(CHANNEL_ID_TEMPERATURE_ALARM, tem, config.getTemperatureWarningSettings());
|
||||
updateAlarm(CHANNEL_ID_HUMIDITY_ALARM, hum, config.getHumidityWarningSettings());
|
||||
}
|
||||
}
|
||||
|
||||
private <T extends Quantity<T>> void updateAlarm(String channelName, QuantityType<T> quantity,
|
||||
WarningSettingsDTO<T> settings) {
|
||||
boolean outOfRange = quantity.compareTo(settings.min) < 0 || settings.max.compareTo(quantity) < 0;
|
||||
updateState(channelName, OnOffType.from(outOfRange));
|
||||
}
|
||||
|
||||
private int scanPacketSize() {
|
||||
switch (model) {
|
||||
case B5175:
|
||||
case B5178:
|
||||
return 10;
|
||||
case H5179:
|
||||
return 8;
|
||||
default:
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
|
||||
super.onScanRecordReceived(scanNotification);
|
||||
byte[] scanData = scanNotification.getData();
|
||||
int dataPacketSize = scanPacketSize();
|
||||
int recordIndex = indexOfTemHumRecord(scanData);
|
||||
if (recordIndex == -1 || recordIndex + dataPacketSize >= scanData.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer data = ByteBuffer.wrap(scanData, recordIndex, dataPacketSize);
|
||||
|
||||
short temperature;
|
||||
int humidity;
|
||||
int battery;
|
||||
int wifiLevel = 0;
|
||||
|
||||
switch (model) {
|
||||
default:
|
||||
data.position(2);// we throw this away
|
||||
// fall through
|
||||
case H5072:
|
||||
case H5075:
|
||||
data.order(ByteOrder.BIG_ENDIAN);
|
||||
int l = data.getInt();
|
||||
l = l & 0xFFFFFF;
|
||||
|
||||
boolean positive = (l & 0x800000) == 0;
|
||||
int tem = (short) ((l / 1000) * 10);
|
||||
if (!positive) {
|
||||
tem = -tem;
|
||||
}
|
||||
temperature = (short) tem;
|
||||
humidity = (l % 1000) * 10;
|
||||
battery = data.get();
|
||||
break;
|
||||
case H5179:
|
||||
data.order(ByteOrder.LITTLE_ENDIAN);
|
||||
data.position(3);
|
||||
temperature = data.getShort();
|
||||
humidity = data.getShort();
|
||||
battery = Byte.toUnsignedInt(data.get());
|
||||
break;
|
||||
case H5051:
|
||||
case H5052:
|
||||
case H5071:
|
||||
case H5074:
|
||||
data.order(ByteOrder.LITTLE_ENDIAN);
|
||||
boolean hasWifi = data.get() == 0;
|
||||
temperature = data.getShort();
|
||||
humidity = Short.toUnsignedInt(data.getShort());
|
||||
battery = Byte.toUnsignedInt(data.get());
|
||||
wifiLevel = hasWifi ? Byte.toUnsignedInt(data.get()) : 0;
|
||||
break;
|
||||
}
|
||||
updateTemHumBattery(temperature, humidity, battery, wifiLevel);
|
||||
}
|
||||
|
||||
private static int indexOfTemHumRecord(byte @Nullable [] scanData) {
|
||||
if (scanData == null || scanData.length != 62) {
|
||||
return -1;
|
||||
}
|
||||
int i = 0;
|
||||
while (i < 57) {
|
||||
int recordLength = scanData[i] & 0xFF;
|
||||
if (scanData[i + 1] == SCAN_HEADER[0]//
|
||||
&& scanData[i + 2] == SCAN_HEADER[1]//
|
||||
&& scanData[i + 3] == SCAN_HEADER[2]) {
|
||||
return i + 4;
|
||||
}
|
||||
|
||||
i += recordLength + 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void updateTemHumBattery(short tem, int hum, int battery, int wifiLevel) {
|
||||
if (Short.toUnsignedInt(tem) == 0xFFFF || hum == 0xFFFF) {
|
||||
logger.trace("Govee device [{}] received invalid data", this.address);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Govee device [{}] received broadcast: tem = {}, hum = {}, battery = {}, wifiLevel = {}",
|
||||
this.address, tem, hum, battery, wifiLevel);
|
||||
|
||||
if (tem == 0 && hum == 0 && battery == 0) {
|
||||
logger.trace("Govee device [{}] values are zero", this.address);
|
||||
return;
|
||||
}
|
||||
if (tem < -4000 || tem > 10000) {
|
||||
logger.trace("Govee device [{}] invalid temperature value: {}", this.address, tem);
|
||||
return;
|
||||
}
|
||||
if (hum > 10000) {
|
||||
logger.trace("Govee device [{}] invalid humidity valie: {}", this.address, hum);
|
||||
return;
|
||||
}
|
||||
|
||||
TemHumDTO temhum = new TemHumDTO();
|
||||
temhum.temperature = new QuantityType<>(tem / 100.0, SIUnits.CELSIUS);
|
||||
temhum.humidity = new QuantityType<>(hum / 100.0, Units.PERCENT);
|
||||
updateTemperatureAndHumidity(temhum, null);
|
||||
|
||||
updateBattery(new QuantityType<>(battery, Units.PERCENT), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
|
||||
super.onCharacteristicUpdate(characteristic);
|
||||
commandSocket.receivePacket(characteristic.getByteValue());
|
||||
}
|
||||
|
||||
private class CommandSocket extends SimpleGattSocket<GoveeMessage> {
|
||||
|
||||
@Override
|
||||
protected ScheduledExecutorService getScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(MessageServicer<GoveeMessage, GoveeMessage> messageServicer) {
|
||||
logger.debug("sending message: {}", messageServicer.getClass().getSimpleName());
|
||||
super.sendMessage(messageServicer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parsePacket(byte[] packet, Consumer<GoveeMessage> messageHandler) {
|
||||
messageHandler.accept(new GoveeMessage(packet));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<@Nullable Void> sendPacket(byte[] data) {
|
||||
return writeCharacteristic(SERVICE_UUID, PROTOCOL_CHAR_UUID, data, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import static org.openhab.binding.bluetooth.govee.internal.GoveeBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum GoveeModel {
|
||||
H5051(THING_TYPE_HYGROMETER, "Govee Wi-Fi Temperature Humidity Monitor", false),
|
||||
H5052(THING_TYPE_HYGROMETER_MONITOR, "Govee Temperature Humidity Monitor", true),
|
||||
H5071(THING_TYPE_HYGROMETER, "Govee Temperature Humidity Monitor", false),
|
||||
H5072(THING_TYPE_HYGROMETER_MONITOR, "Govee Temperature Humidity Monitor", true),
|
||||
H5074(THING_TYPE_HYGROMETER_MONITOR, "Govee Mini Temperature Humidity Monitor", true),
|
||||
H5075(THING_TYPE_HYGROMETER_MONITOR, "Govee Temperature Humidity Monitor", true),
|
||||
H5101(THING_TYPE_HYGROMETER_MONITOR, "Govee Smart Thermo-Hygrometer", true),
|
||||
H5102(THING_TYPE_HYGROMETER_MONITOR, "Govee Smart Thermo-Hygrometer", true),
|
||||
H5177(THING_TYPE_HYGROMETER_MONITOR, "Govee Smart Thermo-Hygrometer", true),
|
||||
H5179(THING_TYPE_HYGROMETER_MONITOR, "Govee Smart Thermo-Hygrometer", true),
|
||||
B5175(THING_TYPE_HYGROMETER_MONITOR, "Govee Smart Thermo-Hygrometer", true),
|
||||
B5178(THING_TYPE_HYGROMETER_MONITOR, "Govee Smart Thermo-Hygrometer", true);
|
||||
|
||||
private final ThingTypeUID thingTypeUID;
|
||||
private final String label;
|
||||
private final boolean supportsWarningBroadcast;
|
||||
|
||||
private GoveeModel(ThingTypeUID thingTypeUID, String label, boolean supportsWarningBroadcast) {
|
||||
this.thingTypeUID = thingTypeUID;
|
||||
this.label = label;
|
||||
this.supportsWarningBroadcast = supportsWarningBroadcast;
|
||||
}
|
||||
|
||||
public ThingTypeUID getThingTypeUID() {
|
||||
return thingTypeUID;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public boolean supportsWarningBroadcast() {
|
||||
return supportsWarningBroadcast;
|
||||
}
|
||||
|
||||
public static @Nullable GoveeModel getGoveeModel(BluetoothDiscoveryDevice device) {
|
||||
String name = device.getName();
|
||||
if (name != null) {
|
||||
if (name.startsWith("Govee") && name.length() >= 11) {
|
||||
String uname = name.toUpperCase();
|
||||
for (GoveeModel model : GoveeModel.values()) {
|
||||
if (uname.contains(model.name())) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetBatteryCommand extends GetCommand {
|
||||
|
||||
private CompletableFuture<@Nullable QuantityType<Dimensionless>> resultHandler;
|
||||
|
||||
public GetBatteryCommand(CompletableFuture<@Nullable QuantityType<Dimensionless>> resultHandler) {
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandCode() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(byte @Nullable [] data, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
resultHandler.completeExceptionally(th);
|
||||
}
|
||||
if (data != null) {
|
||||
int value = data[0] & 0xFF;
|
||||
resultHandler.complete(new QuantityType<Dimensionless>(value, Units.PERCENT));
|
||||
} else {
|
||||
resultHandler.complete(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class GetCommand extends GoveeCommand {
|
||||
|
||||
@Override
|
||||
public byte getCommandType() {
|
||||
return READ_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte @Nullable [] getData() {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetOrSetHumCaliCommand extends GoveeCommand {
|
||||
|
||||
private final CompletableFuture<@Nullable QuantityType<Dimensionless>> resultHandler;
|
||||
private final @Nullable QuantityType<Dimensionless> value;
|
||||
|
||||
public GetOrSetHumCaliCommand(CompletableFuture<@Nullable QuantityType<Dimensionless>> resultHandler) {
|
||||
this.value = null;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
public GetOrSetHumCaliCommand(QuantityType<Dimensionless> value,
|
||||
CompletableFuture<@Nullable QuantityType<Dimensionless>> resultHandler) {
|
||||
this.value = value;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandType() {
|
||||
return value != null ? WRITE_TYPE : READ_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandCode() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
private static short convertQuantity(QuantityType<Dimensionless> quantity) {
|
||||
var percentQuantity = quantity.toUnit(Units.PERCENT);
|
||||
if (percentQuantity == null) {
|
||||
throw new IllegalArgumentException("Unable to convert quantity to percent");
|
||||
}
|
||||
return (short) (percentQuantity.doubleValue() * 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte @Nullable [] getData() {
|
||||
var v = value;
|
||||
if (v != null) {
|
||||
return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(convertQuantity(v)).array();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(byte @Nullable [] data, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
resultHandler.completeExceptionally(th);
|
||||
}
|
||||
if (data != null) {
|
||||
short hum = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).getShort();
|
||||
resultHandler.complete(new QuantityType<>(hum / 100.0, Units.PERCENT));
|
||||
} else {
|
||||
resultHandler.complete(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetOrSetHumWarningCommand extends GoveeCommand {
|
||||
|
||||
private final @Nullable WarningSettingsDTO<Dimensionless> settings;
|
||||
private final CompletableFuture<@Nullable WarningSettingsDTO<Dimensionless>> resultHandler;
|
||||
|
||||
public GetOrSetHumWarningCommand(CompletableFuture<@Nullable WarningSettingsDTO<Dimensionless>> resultHandler) {
|
||||
this.settings = null;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
public GetOrSetHumWarningCommand(WarningSettingsDTO<Dimensionless> settings,
|
||||
CompletableFuture<@Nullable WarningSettingsDTO<Dimensionless>> resultHandler) {
|
||||
this.settings = settings;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandType() {
|
||||
return settings == null ? READ_TYPE : WRITE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandCode() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
private static short convertQuantity(QuantityType<Dimensionless> quantity) {
|
||||
var percentQuantity = quantity.toUnit(Units.PERCENT);
|
||||
if (percentQuantity == null) {
|
||||
throw new IllegalArgumentException("Unable to convert quantity to percent");
|
||||
}
|
||||
return (short) (percentQuantity.doubleValue() * 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte @Nullable [] getData() {
|
||||
if (settings == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.put(settings.enableAlarm == OnOffType.ON ? (byte) 1 : 0);
|
||||
buffer.putShort(convertQuantity(settings.min));
|
||||
buffer.putShort(convertQuantity(settings.max));
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(byte @Nullable [] data, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
resultHandler.completeExceptionally(th);
|
||||
}
|
||||
if (data != null) {
|
||||
WarningSettingsDTO<Dimensionless> result = new WarningSettingsDTO<Dimensionless>();
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.enableAlarm = OnOffType.from(buffer.get() == 1);
|
||||
result.min = new QuantityType<Dimensionless>(buffer.getShort() / 100.0, Units.PERCENT);
|
||||
result.max = new QuantityType<Dimensionless>(buffer.getShort() / 100.0, Units.PERCENT);
|
||||
|
||||
resultHandler.complete(result);
|
||||
} else {
|
||||
resultHandler.complete(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetOrSetTemCaliCommand extends GoveeCommand {
|
||||
private final CompletableFuture<@Nullable QuantityType<Temperature>> resultHandler;
|
||||
private final @Nullable QuantityType<Temperature> value;
|
||||
|
||||
public GetOrSetTemCaliCommand(CompletableFuture<@Nullable QuantityType<Temperature>> resultHandler) {
|
||||
this.value = null;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
public GetOrSetTemCaliCommand(QuantityType<Temperature> value,
|
||||
CompletableFuture<@Nullable QuantityType<Temperature>> resultHandler) {
|
||||
this.value = value;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandType() {
|
||||
return value != null ? WRITE_TYPE : READ_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandCode() {
|
||||
return 7;
|
||||
}
|
||||
|
||||
private static short convertQuantity(QuantityType<Temperature> quantity) {
|
||||
var celciusQuantity = quantity.toUnit(SIUnits.CELSIUS);
|
||||
if (celciusQuantity == null) {
|
||||
throw new IllegalArgumentException("Unable to convert quantity to celcius");
|
||||
}
|
||||
return (short) (celciusQuantity.doubleValue() * 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte @Nullable [] getData() {
|
||||
var v = value;
|
||||
if (v != null) {
|
||||
return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(convertQuantity(v)).array();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(byte @Nullable [] data, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
resultHandler.completeExceptionally(th);
|
||||
}
|
||||
if (data != null) {
|
||||
short tem = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN).getShort();
|
||||
resultHandler.complete(new QuantityType<>(tem / 100.0, SIUnits.CELSIUS));
|
||||
} else {
|
||||
resultHandler.complete(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetOrSetTemWarningCommand extends GoveeCommand {
|
||||
private final @Nullable WarningSettingsDTO<Temperature> settings;
|
||||
private final CompletableFuture<@Nullable WarningSettingsDTO<Temperature>> resultHandler;
|
||||
|
||||
public GetOrSetTemWarningCommand(CompletableFuture<@Nullable WarningSettingsDTO<Temperature>> resultHandler) {
|
||||
this.settings = null;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
public GetOrSetTemWarningCommand(WarningSettingsDTO<Temperature> settings,
|
||||
CompletableFuture<@Nullable WarningSettingsDTO<Temperature>> resultHandler) {
|
||||
this.settings = settings;
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandType() {
|
||||
return settings == null ? READ_TYPE : WRITE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandCode() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
private static short convertQuantity(QuantityType<Temperature> quantity) {
|
||||
var celciusQuantity = quantity.toUnit(SIUnits.CELSIUS);
|
||||
if (celciusQuantity == null) {
|
||||
throw new IllegalArgumentException("Unable to convert quantity to celcius");
|
||||
}
|
||||
return (short) (celciusQuantity.doubleValue() * 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte @Nullable [] getData() {
|
||||
var settings = this.settings;
|
||||
if (settings == null) {
|
||||
return null;
|
||||
}
|
||||
ByteBuffer buffer = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buffer.put(settings.enableAlarm == OnOffType.ON ? (byte) 1 : 0);
|
||||
buffer.putShort(convertQuantity(settings.min));
|
||||
buffer.putShort(convertQuantity(settings.max));
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(byte @Nullable [] data, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
resultHandler.completeExceptionally(th);
|
||||
}
|
||||
if (data != null) {
|
||||
WarningSettingsDTO<Temperature> result = new WarningSettingsDTO<Temperature>();
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
|
||||
result.enableAlarm = OnOffType.from(buffer.get() == 1);
|
||||
result.min = new QuantityType<Temperature>(buffer.getShort() / 100.0, SIUnits.CELSIUS);
|
||||
result.max = new QuantityType<Temperature>(buffer.getShort() / 100.0, SIUnits.CELSIUS);
|
||||
|
||||
resultHandler.complete(result);
|
||||
} else {
|
||||
resultHandler.complete(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GetTemHumCommand extends GetCommand {
|
||||
|
||||
private CompletableFuture<@Nullable TemHumDTO> resultHandler;
|
||||
|
||||
public GetTemHumCommand(CompletableFuture<@Nullable TemHumDTO> resultHandler) {
|
||||
this.resultHandler = resultHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getCommandCode() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(byte @Nullable [] data, @Nullable Throwable th) {
|
||||
if (th != null) {
|
||||
resultHandler.completeExceptionally(th);
|
||||
}
|
||||
if (data != null) {
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
int temp = buffer.getShort();
|
||||
int hum = Short.toUnsignedInt(buffer.getShort());
|
||||
|
||||
TemHumDTO temhum = new TemHumDTO();
|
||||
temhum.temperature = new QuantityType<>(temp / 100.0, SIUnits.CELSIUS);
|
||||
temhum.humidity = new QuantityType<>(hum / 100.0, Units.PERCENT);
|
||||
resultHandler.complete(temhum);
|
||||
} else {
|
||||
resultHandler.complete(null);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.gattserial.SimpleMessageServicer;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class GoveeCommand implements SimpleMessageServicer<GoveeMessage> {
|
||||
|
||||
public static final byte READ_TYPE = -86;
|
||||
public static final byte WRITE_TYPE = 51;
|
||||
|
||||
public abstract byte getCommandType();
|
||||
|
||||
public abstract byte getCommandCode();
|
||||
|
||||
protected abstract byte @Nullable [] getData();
|
||||
|
||||
@Override
|
||||
public long getTimeout(TimeUnit unit) {
|
||||
return unit.convert(60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GoveeMessage createMessage() {
|
||||
return new GoveeMessage(getCommandType(), getCommandCode(), getData());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleFailedMessage(GoveeMessage message, Throwable th) {
|
||||
if (matches(message)) {
|
||||
handleResponse(null, th);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handleReceivedMessage(GoveeMessage message) {
|
||||
if (matches(message)) {
|
||||
handleResponse(message.getData(), null);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract void handleResponse(byte @Nullable [] data, @Nullable Throwable th);
|
||||
|
||||
protected boolean matches(GoveeMessage message) {
|
||||
return message.getCommandType() == getCommandType() && message.getCommandCode() == getCommandCode();
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.bluetooth.gattserial.GattMessage;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class GoveeMessage implements GattMessage {
|
||||
|
||||
private byte[] payload;
|
||||
|
||||
public GoveeMessage(byte[] payload) {
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public GoveeMessage(byte commandType, byte commandCode, byte @Nullable [] data) {
|
||||
payload = new byte[20];
|
||||
payload[0] = commandType;
|
||||
payload[1] = commandCode;
|
||||
if (data != null) {
|
||||
System.arraycopy(data, 0, payload, 2, data.length);
|
||||
}
|
||||
payload[19] = calculateCrc(payload, 19);
|
||||
}
|
||||
|
||||
public byte getCommandType() {
|
||||
return payload[0];
|
||||
}
|
||||
|
||||
public byte getCommandCode() {
|
||||
return payload[1];
|
||||
}
|
||||
|
||||
protected static byte calculateCrc(byte[] bArr, int i) {
|
||||
byte b = bArr[0];
|
||||
for (int i2 = 1; i2 < i; i2++) {
|
||||
b = (byte) (b ^ bArr[i2]);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
public byte @Nullable [] getData() {
|
||||
byte[] data = new byte[17];
|
||||
System.arraycopy(payload, 2, data, 0, Math.min(payload.length - 2, 17));
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getPayload() {
|
||||
return payload;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class SetCommand extends GoveeCommand {
|
||||
|
||||
@Override
|
||||
public byte getCommandType() {
|
||||
return WRITE_TYPE;
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class TemHumDTO {
|
||||
public QuantityType<@NonNull Temperature> temperature;
|
||||
public QuantityType<@NonNull Dimensionless> humidity;
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.command.hygrometer;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class WarningSettingsDTO<Q extends Quantity<Q>> {
|
||||
public OnOffType enableAlarm;
|
||||
public QuantityType<Q> min;
|
||||
public QuantityType<Q> max;
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="bluetooth"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
|
||||
<thing-type id="goveeHygrometer">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="roaming"/>
|
||||
<bridge-type-ref id="bluegiga"/>
|
||||
<bridge-type-ref id="bluez"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Govee Hygrometer</label>
|
||||
<description>Govee Thermo-Hygrometer</description>
|
||||
|
||||
<channels>
|
||||
<channel id="rssi" typeId="rssi"/>
|
||||
<channel id="battery" typeId="system.battery-level"/>
|
||||
|
||||
<channel id="temperature" typeId="govee-temperature"/>
|
||||
<channel id="humidity" typeId="system.atmospheric-humidity"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>address</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter-group name="calibration">
|
||||
<label>Calibration</label>
|
||||
<description>Sensor calibration settings.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
<parameter name="address" type="text" required="true">
|
||||
<label>Address</label>
|
||||
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" unit="s" required="true">
|
||||
<label>Refresh Interval</label>
|
||||
<description>The frequency at which battery, temperature, and humidity data will refresh</description>
|
||||
<default>300</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="temperatureCalibration" type="decimal" min="-1.6" max="1.6" groupName="calibration"
|
||||
unit="Cel">
|
||||
<label>Temperature Calibration</label>
|
||||
<description>Adds offset to reported temperature</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="humidityCalibration" type="decimal" min="-9" max="9" groupName="calibration" unit="%">
|
||||
<label>Humidity Calibration</label>
|
||||
<description>Adds offset to reported humidity</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="goveeHygrometerMonitor">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="roaming"/>
|
||||
<bridge-type-ref id="bluegiga"/>
|
||||
<bridge-type-ref id="bluez"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>Govee Monitoring Hygrometer</label>
|
||||
<description>Govee Thermo-Hygrometer w/ Warning Alarms</description>
|
||||
|
||||
<channels>
|
||||
<channel id="rssi" typeId="rssi"/>
|
||||
<channel id="battery" typeId="system.battery-level"/>
|
||||
|
||||
<channel id="temperature" typeId="govee-temperature"/>
|
||||
<channel id="temperatureAlarm" typeId="govee-temperature-alarm"/>
|
||||
|
||||
<channel id="humidity" typeId="system.atmospheric-humidity"/>
|
||||
<channel id="humidityAlarm" typeId="govee-humidity-alarm"/>
|
||||
|
||||
</channels>
|
||||
|
||||
<representation-property>address</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter-group name="calibration">
|
||||
<label>Calibration</label>
|
||||
<description>Sensor calibration settings.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
<parameter-group name="alarms">
|
||||
<label>Alarm</label>
|
||||
<description>Alarm settings.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter-group>
|
||||
|
||||
|
||||
<parameter name="address" type="text" required="true">
|
||||
<label>Address</label>
|
||||
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" unit="s" required="true">
|
||||
<label>Refresh Interval</label>
|
||||
<description>The frequency at which battery, temperature, and humidity data will refresh</description>
|
||||
<default>300</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="temperatureCalibration" type="decimal" min="-1.6" max="1.6" groupName="calibration"
|
||||
unit="Cel">
|
||||
<label>Temperature Calibration</label>
|
||||
<description>Adds offset to reported temperature</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="temperatureWarningAlarm" type="boolean" groupName="alarms" required="true">
|
||||
<label>Broadcast Temperature Warning</label>
|
||||
<description>If enabled, the Govee device will notify openHAB if temperature is out of the specified range</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="temperatureWarningMin" type="decimal" min="-20" max="60" step="0.2" groupName="alarms"
|
||||
unit="Cel" required="true">
|
||||
<label>Min Warning Temperature</label>
|
||||
<description>Sets the lowest acceptable temperature value before a warning should be issued</description>
|
||||
<default>0</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="temperatureWarningMax" type="decimal" min="-20" max="60" step="0.2" groupName="alarms"
|
||||
unit="Cel" required="true">
|
||||
<label>Max Warning Temperature</label>
|
||||
<description>Sets the highest acceptable temperature value before a warning should be issued</description>
|
||||
<default>0</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
|
||||
<parameter name="humidityCalibration" type="decimal" min="-9" max="9" groupName="calibration" unit="%">
|
||||
<label>Humidity Calibration</label>
|
||||
<description>Adds offset to reported humidity</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="humidityWarningAlarm" type="boolean" groupName="alarms" required="true">
|
||||
<label>Broadcast Humidity Warning</label>
|
||||
<description>If enabled, the Govee device will notify openHAB if humidity is out of the specified range</description>
|
||||
<default>false</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="humidityWarningMin" type="decimal" min="0" max="100" step="0.1" groupName="alarms"
|
||||
unit="%" required="true">
|
||||
<label>Min Warning Humidity</label>
|
||||
<description>Sets the lowest acceptable humidity value before a warning should be issued</description>
|
||||
<default>0</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="humidityWarningMax" type="decimal" min="0" max="100" step="0.1" groupName="alarms"
|
||||
unit="%" required="true">
|
||||
<label>Max Warning Humidity</label>
|
||||
<description>Sets the highest acceptable humidity value before a warning should be issued</description>
|
||||
<default>0</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="govee-temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Current Measured Temperature</label>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="govee-temperature-alarm">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Temperature Warning Alarm</label>
|
||||
<description>
|
||||
If temperature warnings are enabled, then this alarm indicates whether the current temperature is out of
|
||||
range.
|
||||
</description>
|
||||
<category>Alarm</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="govee-humidity-alarm">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Humidity Warning Alarm</label>
|
||||
<description>
|
||||
If humidity warnings are enabled, then this alarm indicates whether the current humidity is out of range.
|
||||
</description>
|
||||
<category>Alarm</category>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.bluetooth.MockBluetoothAdapter;
|
||||
import org.openhab.binding.bluetooth.MockBluetoothDevice;
|
||||
import org.openhab.binding.bluetooth.TestUtils;
|
||||
import org.openhab.binding.bluetooth.discovery.BluetoothDiscoveryDevice;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class GoveeModelTest {
|
||||
|
||||
// the participant is stateless so this is fine.
|
||||
// private GoveeDiscoveryParticipant participant = new GoveeDiscoveryParticipant();
|
||||
|
||||
@Test
|
||||
void noMatchTest() {
|
||||
MockBluetoothAdapter adapter = new MockBluetoothAdapter();
|
||||
MockBluetoothDevice mockDevice = adapter.getDevice(TestUtils.randomAddress());
|
||||
mockDevice.setName("asdfasdf");
|
||||
|
||||
Assertions.assertNull(GoveeModel.getGoveeModel(new BluetoothDiscoveryDevice(mockDevice)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGovee_H5074_84DD() {
|
||||
MockBluetoothAdapter adapter = new MockBluetoothAdapter();
|
||||
MockBluetoothDevice mockDevice = adapter.getDevice(TestUtils.randomAddress());
|
||||
mockDevice.setName("Govee_H5074_84DD");
|
||||
|
||||
Assertions.assertEquals(GoveeModel.H5074, GoveeModel.getGoveeModel(new BluetoothDiscoveryDevice(mockDevice)));
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2021 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.govee.internal.readme;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpression;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.openhab.binding.bluetooth.govee.internal.GoveeModel;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
/**
|
||||
* @author Connor Petty - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class ThingTypeTableGenerator {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
|
||||
FileInputStream fileIS = new FileInputStream("src/main/resources/OH-INF/thing/thing-types.xml");
|
||||
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = builderFactory.newDocumentBuilder();
|
||||
Document xmlDocument = builder.parse(fileIS);
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
String expression = "/*[local-name()='thing-descriptions']/thing-type";
|
||||
XPathExpression labelExpression = xPath.compile("label/text()");
|
||||
XPathExpression descriptionExpression = xPath.compile("description/text()");
|
||||
|
||||
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);
|
||||
|
||||
List<ThingTypeData> thingTypeDataList = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
Node node = nodeList.item(i);
|
||||
ThingTypeData data = new ThingTypeData();
|
||||
|
||||
data.id = node.getAttributes().getNamedItem("id").getTextContent();
|
||||
data.label = (String) labelExpression.evaluate(node, XPathConstants.STRING);
|
||||
data.description = (String) descriptionExpression.evaluate(node, XPathConstants.STRING);
|
||||
|
||||
thingTypeDataList.add(data);
|
||||
}
|
||||
|
||||
String[] headerRow = new String[] { "Thing Type ID", "Description", "Supported Models" };
|
||||
|
||||
List<String[]> rows = new ArrayList<>();
|
||||
rows.add(headerRow);
|
||||
rows.addAll(thingTypeDataList.stream().map(ThingTypeTableGenerator::toRow).collect(Collectors.toList()));
|
||||
|
||||
int[] maxColumns = { maxColumnSize(rows, 0), maxColumnSize(rows, 1), maxColumnSize(rows, 2) };
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
// write actual rows
|
||||
rows.forEach(row -> {
|
||||
writer.append(writeRow(maxColumns, row, ' ')).append('\n');
|
||||
if (row == headerRow) {
|
||||
writer.append(writeRow(maxColumns, new String[] { "", "", "" }, '-')).append('\n');
|
||||
}
|
||||
});
|
||||
|
||||
System.out.println(writer.toString());
|
||||
}
|
||||
|
||||
private static String writeRow(int[] maxColumns, String[] row, char paddingChar) {
|
||||
String prefix = "|" + paddingChar;
|
||||
String infix = paddingChar + "|" + paddingChar;
|
||||
String suffix = paddingChar + "|";
|
||||
|
||||
return Stream.of(0, 1, 2).map(i -> rightPad(row[i], maxColumns[i], paddingChar))
|
||||
.collect(Collectors.joining(infix, prefix, suffix));
|
||||
}
|
||||
|
||||
private static String rightPad(String str, int minLength, char paddingChar) {
|
||||
if (str.length() >= minLength) {
|
||||
return str;
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(minLength);
|
||||
builder.append(str);
|
||||
while (builder.length() < minLength) {
|
||||
builder.append(paddingChar);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static int maxColumnSize(List<String[]> rows, int column) {
|
||||
return rows.stream().map(row -> row[column].length()).max(Integer::compare).get();
|
||||
}
|
||||
|
||||
private static class ThingTypeData {
|
||||
private String id;
|
||||
private String label;
|
||||
private String description;
|
||||
}
|
||||
|
||||
private static String[] toRow(ThingTypeData data) {
|
||||
return new String[] { data.id, //
|
||||
data.description, //
|
||||
modelsForType(data.id).stream().map(model -> model.name()).collect(Collectors.joining(",")) };
|
||||
}
|
||||
|
||||
private static List<GoveeModel> modelsForType(String typeUID) {
|
||||
return Arrays.stream(GoveeModel.values()).filter(model -> model.getThingTypeUID().getId().equals(typeUID))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
@ -14,4 +14,19 @@
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Bluetooth Binding</name>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
@ -63,6 +63,7 @@
|
||||
<module>org.openhab.binding.bluetooth.daikinmadoka</module>
|
||||
<module>org.openhab.binding.bluetooth.enoceanble</module>
|
||||
<module>org.openhab.binding.bluetooth.generic</module>
|
||||
<module>org.openhab.binding.bluetooth.govee</module>
|
||||
<module>org.openhab.binding.bluetooth.roaming</module>
|
||||
<module>org.openhab.binding.bluetooth.ruuvitag</module>
|
||||
<module>org.openhab.binding.boschindego</module>
|
||||
|
@ -12,6 +12,7 @@
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.daikinmadoka/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.enoceanble/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.generic/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.govee/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.roaming/${project.version}</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.ruuvitag/${project.version}</bundle>
|
||||
</feature>
|
||||
|
Loading…
Reference in New Issue
Block a user