[fineoffsetweatherstation] Add support for ELV protocol (#13138)

* [fineoffsetweatherstation] add support for ELV protocol
* [fineoffsetweatherstation] remove german translations
* [fineoffsetweatherstation] add representationProperty for gateway, fix unit of irradiation-uv
* [fineoffsetweatherstation] add representationProperties in descriptor xml file
* [fineoffsetweatherstation] set channels values to undef in case of communication errors
* [fineoffsetweatherstation] use lock for request synchronisation
* [fineoffsetweatherstation] marke sensors as gone, if they got unpaired

resolves #12763

Signed-off-by: Andreas Berger <andreas@berger-freelancer.com>
This commit is contained in:
Andreas Berger 2022-07-25 16:39:52 +02:00 committed by GitHub
parent 9ec6f7d12e
commit 84ea355e76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 762 additions and 530 deletions

View File

@ -7,6 +7,7 @@ Some of these brands are e.g.:
* Aercus * Aercus
* Ambient Weather * Ambient Weather
* Ecowitt * Ecowitt
* ELV
* Frogitt * Frogitt
* Misol * Misol
* Pantech * Pantech
@ -40,6 +41,7 @@ This binding works offline by [implementing the wire protocol](https://osswww.ec
- WH2680 - WH2680
- WH2900 - WH2900
- WH2950 - WH2950
- WS980 ELV (tested)
- `sensor`: A Fine Offset sensor which is connected to the bridge with the ThingTypeUID `fineoffsetweatherstation:sensor`. - `sensor`: A Fine Offset sensor which is connected to the bridge with the ThingTypeUID `fineoffsetweatherstation:sensor`.
Since the gateway collects all the sensor data and harmonizes them, the sensor thing itself will only hold information about the signal and battery status. Since the gateway collects all the sensor data and harmonizes them, the sensor thing itself will only hold information about the signal and battery status.
This is a list of sensors supported by the protocol: This is a list of sensors supported by the protocol:
@ -68,12 +70,13 @@ This binding support discovery of Fine Offset gateway devices by sending a broad
### `gateway` Thing Configuration ### `gateway` Thing Configuration
| Name | Type | Description | Default | Required | Advanced | | Name | Type | Description | Default | Required | Advanced |
|-------------------|---------|-------------------------------------------------------------------------------------|---------|----------|----------| |------------------|---------|----------------------------------------------------------------------------------------------|---------|----------|----------|
| ip | text | The Hostname or IP address of the device | N/A | yes | no | | ip | text | The Hostname or IP address of the device | N/A | yes | no |
| port | integer | The network port of the gateway | 45000 | yes | no | | port | integer | The network port of the gateway | 45000 | yes | no |
| pollingInterval | integer | Polling period for refreshing the data in seconds | 16 | yes | yes | | protocol | text | The protocol to use for communicating with the gateway, valid values are: `DEFAULT` or `ELV` | DEFAULT | no | no |
| discoverInterval | integer | Interval in seconds to fetch registered sensors, battery status and signal strength | 900 | yes | yes | | pollingInterval | integer | Polling period for refreshing the data in seconds | 16 | yes | yes |
| discoverInterval | integer | Interval in seconds to fetch registered sensors, battery status and signal strength | 900 | yes | yes |
### `sensor` Thing Configuration ### `sensor` Thing Configuration
@ -267,7 +270,13 @@ This is an example configuration for the WH2650 gateway
_weatherstation.things_: _weatherstation.things_:
```xtend ```xtend
Bridge fineoffsetweatherstation:gateway:3906700515 "Weather station" [ip="192.168.1.42", port="45000", discoverInterval="900", pollingInterval="16"] { Bridge fineoffsetweatherstation:gateway:3906700515 "Weather station" [
ip="192.168.1.42",
port="45000",
discoverInterval="900",
pollingInterval="16",
protocol="DEFAULT"
] {
Thing sensor WH25 "WH25" [sensor="WH25"] Thing sensor WH25 "WH25" [sensor="WH25"]
Thing sensor WH65 "WH65" [sensor="WH65"] Thing sensor WH65 "WH65" [sensor="WH65"]
} }

View File

@ -26,6 +26,11 @@
<name>openHAB Add-ons :: Bundles :: Fine Offset Weather Station</name> <name>openHAB Add-ons :: Bundles :: Fine Offset Weather Station</name>
<dependencies> <dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.9.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.assertj</groupId> <groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId> <artifactId>assertj-core</artifactId>

View File

@ -14,6 +14,7 @@ package org.openhab.binding.fineoffsetweatherstation.internal;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Protocol;
/** /**
* The {@link FineOffsetGatewayConfiguration} class contains fields mapping thing configuration parameters. * The {@link FineOffsetGatewayConfiguration} class contains fields mapping thing configuration parameters.
@ -26,8 +27,12 @@ public class FineOffsetGatewayConfiguration {
public static final String IP = "ip"; public static final String IP = "ip";
public static final String PORT = "port"; public static final String PORT = "port";
public static final String PROTOCOL = "protocol";
public @Nullable String ip; public @Nullable String ip;
public int port = 45000; public int port = 45000;
public int pollingInterval = 16; public int pollingInterval = 16;
public int discoverInterval = 900; public int discoverInterval = 900;
public Protocol protocol = Protocol.DEFAULT;
} }

View File

@ -21,10 +21,12 @@ import java.net.DatagramSocket;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketException; import java.net.SocketException;
import java.time.ZoneOffset;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -36,7 +38,12 @@ import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetSensorCon
import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants; import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants;
import org.openhab.binding.fineoffsetweatherstation.internal.Utils; import org.openhab.binding.fineoffsetweatherstation.internal.Utils;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Command; import org.openhab.binding.fineoffsetweatherstation.internal.domain.Command;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Protocol;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice;
import org.openhab.binding.fineoffsetweatherstation.internal.service.GatewayQueryService;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryResultBuilder;
@ -60,13 +67,13 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
@Component(service = { DiscoveryService.class, FineOffsetGatewayDiscoveryService.class }, immediate = true) @Component(service = { DiscoveryService.class, FineOffsetGatewayDiscoveryService.class }, immediate = true)
public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService { public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService {
public static final int DISCOVERY_PORT = 46000; private static final int DISCOVERY_PORT = 46000;
private static final int BUFFER_LENGTH = 255; private static final int BUFFER_LENGTH = 255;
private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayDiscoveryService.class); private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayDiscoveryService.class);
private static final long REFRESH_INTERVAL = 600; private static final long REFRESH_INTERVAL = 600;
private static final int DISCOVERY_TIME = 5; private static final int DISCOVERY_TIME = 10;
private final TranslationProvider translationProvider; private final TranslationProvider translationProvider;
private final LocaleProvider localeProvider; private final LocaleProvider localeProvider;
private final @Nullable Bundle bundle; private final @Nullable Bundle bundle;
@ -124,7 +131,11 @@ public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService
private void discover() { private void discover() {
startReceiverThread(); startReceiverThread();
NetUtil.getAllBroadcastAddresses().forEach(this::sendDiscoveryRequest); NetUtil.getAllBroadcastAddresses().forEach(broadcastAddress -> {
sendBroadcastPacket(broadcastAddress, Command.CMD_BROADCAST.getPayloadAlternative());
scheduler.schedule(() -> sendBroadcastPacket(broadcastAddress, Command.CMD_BROADCAST.getPayload()), 5,
TimeUnit.SECONDS);
});
} }
public void addSensors(ThingUID bridgeUID, Collection<SensorDevice> sensorDevices) { public void addSensors(ThingUID bridgeUID, Collection<SensorDevice> sensorDevices) {
@ -168,15 +179,39 @@ public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService
properties.put(Thing.PROPERTY_MAC_ADDRESS, Utils.toHexString(macAddr, macAddr.length, ":")); properties.put(Thing.PROPERTY_MAC_ADDRESS, Utils.toHexString(macAddr, macAddr.length, ":"));
properties.put(FineOffsetGatewayConfiguration.IP, ip); properties.put(FineOffsetGatewayConfiguration.IP, ip);
properties.put(FineOffsetGatewayConfiguration.PORT, port); properties.put(FineOffsetGatewayConfiguration.PORT, port);
FineOffsetGatewayConfiguration config = new Configuration(properties).as(FineOffsetGatewayConfiguration.class);
Protocol protocol = determineProtocol(config);
if (protocol != null) {
properties.put(FineOffsetGatewayConfiguration.PROTOCOL, protocol.name());
}
ThingUID uid = new ThingUID(THING_TYPE_GATEWAY, id); ThingUID uid = new ThingUID(THING_TYPE_GATEWAY, id);
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties) DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
.withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS)
.withLabel(translationProvider.getText(bundle, "thing.gateway.label", name, localeProvider.getLocale())) .withLabel(translationProvider.getText(bundle, "thing.gateway.label", name, localeProvider.getLocale()))
.build(); .build();
thingDiscovered(result); thingDiscovered(result);
logger.debug("Thing discovered '{}'", result); logger.debug("Thing discovered '{}'", result);
} }
@Nullable
private Protocol determineProtocol(FineOffsetGatewayConfiguration config) {
ConversionContext conversionContext = new ConversionContext(ZoneOffset.UTC);
for (Protocol protocol : Protocol.values()) {
try (GatewayQueryService gatewayQueryService = protocol.getGatewayQueryService(config, null,
conversionContext)) {
List<MeasuredValue> result = gatewayQueryService.getMeasuredValues();
logger.trace("found {} measured values via protocol {}", result.size(), protocol);
if (!result.isEmpty()) {
return protocol;
}
} catch (IOException e) {
logger.warn("", e);
}
}
return null;
}
synchronized @Nullable DatagramSocket getSocket() { synchronized @Nullable DatagramSocket getSocket() {
DatagramSocket clientSocket = this.clientSocket; DatagramSocket clientSocket = this.clientSocket;
if (clientSocket != null && clientSocket.isBound()) { if (clientSocket != null && clientSocket.isBound()) {
@ -205,12 +240,13 @@ public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService
this.clientSocket = null; this.clientSocket = null;
} }
private void sendDiscoveryRequest(String broadcastAddress) { private void sendBroadcastPacket(String broadcastAddress, byte[] requestMessage) {
final @Nullable DatagramSocket socket = getSocket(); final @Nullable DatagramSocket socket = getSocket();
if (socket != null) { if (socket != null) {
byte[] requestMessage = Command.CMD_BROADCAST.getPayload();
InetSocketAddress addr = new InetSocketAddress(broadcastAddress, DISCOVERY_PORT); InetSocketAddress addr = new InetSocketAddress(broadcastAddress, DISCOVERY_PORT);
DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, addr); DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, addr);
logger.trace("sendBroadcastPacket: send request: {}",
Utils.toHexString(requestMessage, requestMessage.length, ""));
try { try {
socket.send(datagramPacket); socket.send(datagramPacket);
} catch (IOException e) { } catch (IOException e) {

View File

@ -12,10 +12,7 @@
*/ */
package org.openhab.binding.fineoffsetweatherstation.internal.domain; package org.openhab.binding.fineoffsetweatherstation.internal.domain;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.Utils; import org.openhab.binding.fineoffsetweatherstation.internal.Utils;
/** /**
@ -25,6 +22,12 @@ import org.openhab.binding.fineoffsetweatherstation.internal.Utils;
*/ */
@NonNullByDefault @NonNullByDefault
public enum Command { public enum Command {
/**
* read current datareply data size is 2bytes.
*/
CMD_WS980_LIVEDATA((byte) 0x0b, 2),
/** /**
* send SSID and Password to WIFI module * send SSID and Password to WIFI module
*/ */
@ -238,21 +241,18 @@ public enum Command {
this.sizeBytes = sizeBytes; this.sizeBytes = sizeBytes;
} }
public byte getCode() {
return code;
}
public int getSizeBytes() {
return sizeBytes;
}
public byte[] getPayload() { public byte[] getPayload() {
byte size = 3; // + rest of payload / not yet implemented byte size = 3; // + rest of payload / not yet implemented
return new byte[] { (byte) 0xff, (byte) 0xff, code, size, (byte) (code + size) }; return new byte[] { (byte) 0xff, (byte) 0xff, code, size, (byte) (code + size) };
} }
public static @Nullable Command findByCode(byte code) { public byte[] getPayloadAlternative() {
return Arrays.stream(values()).filter(command -> command.getCode() == code).findFirst().orElse(null); if (sizeBytes == 2) {
return new byte[] { (byte) 0xff, (byte) 0xff, code, 0, (byte) (sizeBytes + 2),
(byte) ((code + sizeBytes + 2) % 0xff) };
}
byte size = 3;
return new byte[] { (byte) 0xff, (byte) 0xff, code, size, (byte) ((code + size) % 0xff) };
} }
public boolean isHeaderValid(byte[] data) { public boolean isHeaderValid(byte[] data) {

View File

@ -1,391 +0,0 @@
/**
* Copyright (c) 2010-2022 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.fineoffsetweatherstation.internal.domain;
import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_MAX_WIND_SPEED;
import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_MOISTURE;
import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_UV_INDEX;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.State;
/**
* The measurands of supported by the gateway.
*
* @author Andreas Berger - Initial contribution
*/
@NonNullByDefault
public enum Measurand {
INTEMP("temperature-indoor", (byte) 0x01, "Indoor Temperature", MeasureType.TEMPERATURE,
DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_INDOOR_TEMPERATURE),
OUTTEMP("temperature-outdoor", (byte) 0x02, "Outdoor Temperature", MeasureType.TEMPERATURE,
DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_OUTDOOR_TEMPERATURE),
DEWPOINT("temperature-dew-point", (byte) 0x03, "Dew point", MeasureType.TEMPERATURE),
WINDCHILL("temperature-wind-chill", (byte) 0x04, "Wind chill", MeasureType.TEMPERATURE),
HEATINDEX("temperature-heat-index", (byte) 0x05, "Heat index", MeasureType.TEMPERATURE),
INHUMI("humidity-indoor", (byte) 0x06, "Indoor Humidity", MeasureType.PERCENTAGE),
OUTHUMI("humidity-outdoor", (byte) 0x07, "Outdoor Humidity", MeasureType.PERCENTAGE,
DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_ATMOSPHERIC_HUMIDITY),
ABSBARO("pressure-absolute", (byte) 0x08, "Absolutely pressure", MeasureType.PRESSURE),
RELBARO("pressure-relative", (byte) 0x09, "Relative pressure", MeasureType.PRESSURE,
DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_BAROMETRIC_PRESSURE),
WINDDIRECTION("direction-wind", (byte) 0x0A, "Wind Direction", MeasureType.DEGREE,
DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_DIRECTION),
WINDSPEED("speed-wind", (byte) 0x0B, "Wind Speed", MeasureType.SPEED,
DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_SPEED),
GUSTSPEED("speed-gust", (byte) 0x0C, "Gust Speed", MeasureType.SPEED,
DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_SPEED),
RAINEVENT("rain-event", (byte) 0x0D, "Rain Event", MeasureType.HEIGHT),
RAINRATE("rain-rate", (byte) 0x0E, "Rain Rate", MeasureType.HEIGHT_PER_HOUR),
RAINHOUR("rain-hour", (byte) 0x0F, "Rain hour", MeasureType.HEIGHT),
RAINDAY("rain-day", (byte) 0x10, "Rain Day", MeasureType.HEIGHT),
RAINWEEK("rain-week", (byte) 0x11, "Rain Week", MeasureType.HEIGHT),
RAINMONTH("rain-month", (byte) 0x12, "Rain Month", MeasureType.HEIGHT_BIG),
RAINYEAR("rain-year", (byte) 0x13, "Rain Year", MeasureType.HEIGHT_BIG),
RAINTOTALS("rain-total", (byte) 0x14, "Rain Totals", MeasureType.HEIGHT_BIG),
LIGHT("illumination", (byte) 0x15, "Light", MeasureType.LUX),
UV("irradiation-uv", (byte) 0x16, "UV", MeasureType.MICROWATT_PER_SQUARE_CENTIMETRE),
UVI("uv-index", (byte) 0x17, "UV index", MeasureType.BYTE, CHANNEL_TYPE_UV_INDEX),
TIME("time", (byte) 0x18, "Date and time", MeasureType.DATE_TIME2),
DAYLWINDMAX("wind-max-day", (byte) 0X19, "Day max wind", MeasureType.SPEED, CHANNEL_TYPE_MAX_WIND_SPEED),
TEMP1("temperature-channel-1", (byte) 0x1A, "Temperature 1", MeasureType.TEMPERATURE),
TEMP2("temperature-channel-2", (byte) 0x1B, "Temperature 2", MeasureType.TEMPERATURE),
TEMP3("temperature-channel-3", (byte) 0x1C, "Temperature 3", MeasureType.TEMPERATURE),
TEMP4("temperature-channel-4", (byte) 0x1D, "Temperature 4", MeasureType.TEMPERATURE),
TEMP5("temperature-channel-5", (byte) 0x1E, "Temperature 5", MeasureType.TEMPERATURE),
TEMP6("temperature-channel-6", (byte) 0x1F, "Temperature 6", MeasureType.TEMPERATURE),
TEMP7("temperature-channel-7", (byte) 0x20, "Temperature 7", MeasureType.TEMPERATURE),
TEMP8("temperature-channel-8", (byte) 0x21, "Temperature 8", MeasureType.TEMPERATURE),
HUMI1("humidity-channel-1", (byte) 0x22, "Humidity 1", MeasureType.PERCENTAGE),
HUMI2("humidity-channel-2", (byte) 0x23, "Humidity 2", MeasureType.PERCENTAGE),
HUMI3("humidity-channel-3", (byte) 0x24, "Humidity 3", MeasureType.PERCENTAGE),
HUMI4("humidity-channel-4", (byte) 0x25, "Humidity 4", MeasureType.PERCENTAGE),
HUMI5("humidity-channel-5", (byte) 0x26, "Humidity 5", MeasureType.PERCENTAGE),
HUMI6("humidity-channel-6", (byte) 0x27, "Humidity 6", MeasureType.PERCENTAGE),
HUMI7("humidity-channel-7", (byte) 0x28, "Humidity 7", MeasureType.PERCENTAGE),
HUMI8("humidity-channel-8", (byte) 0x29, "Humidity 8", MeasureType.PERCENTAGE),
SOILTEMP1("temperature-soil-channel-1", (byte) 0x2B, "Soil Temperature 1", MeasureType.TEMPERATURE),
SOILTEMP2("temperature-soil-channel-2", (byte) 0x2D, "Soil Temperature 2", MeasureType.TEMPERATURE),
SOILTEMP3("temperature-soil-channel-3", (byte) 0x2F, "Soil Temperature 3", MeasureType.TEMPERATURE),
SOILTEMP4("temperature-soil-channel-4", (byte) 0x31, "Soil Temperature 4", MeasureType.TEMPERATURE),
SOILTEMP5("temperature-soil-channel-5", (byte) 0x33, "Soil Temperature 5", MeasureType.TEMPERATURE),
SOILTEMP6("temperature-soil-channel-6", (byte) 0x35, "Soil Temperature 6", MeasureType.TEMPERATURE),
SOILTEMP7("temperature-soil-channel-7", (byte) 0x37, "Soil Temperature 7", MeasureType.TEMPERATURE),
SOILTEMP8("temperature-soil-channel-8", (byte) 0x39, "Soil Temperature 8", MeasureType.TEMPERATURE),
SOILTEMP9("temperature-soil-channel-9", (byte) 0x3B, "Soil Temperature 9", MeasureType.TEMPERATURE),
SOILTEMP10("temperature-soil-channel-10", (byte) 0x3D, "Soil Temperature 10", MeasureType.TEMPERATURE),
SOILTEMP11("temperature-soil-channel-11", (byte) 0x3F, "Soil Temperature 11", MeasureType.TEMPERATURE),
SOILTEMP12("temperature-soil-channel-12", (byte) 0x41, "Soil Temperature 12", MeasureType.TEMPERATURE),
SOILTEMP13("temperature-soil-channel-13", (byte) 0x43, "Soil Temperature 13", MeasureType.TEMPERATURE),
SOILTEMP14("temperature-soil-channel-14", (byte) 0x45, "Soil Temperature 14", MeasureType.TEMPERATURE),
SOILTEMP15("temperature-soil-channel-15", (byte) 0x47, "Soil Temperature 15", MeasureType.TEMPERATURE),
SOILTEMP16("temperature-soil-channel-16", (byte) 0x49, "Soil Temperature 16", MeasureType.TEMPERATURE),
SOILMOISTURE1("moisture-soil-channel-1", (byte) 0x2C, "Soil Moisture 1", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE2("moisture-soil-channel-2", (byte) 0x2E, "Soil Moisture 2", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE3("moisture-soil-channel-3", (byte) 0x30, "Soil Moisture 3", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE4("moisture-soil-channel-4", (byte) 0x32, "Soil Moisture 4", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE5("moisture-soil-channel-5", (byte) 0x34, "Soil Moisture 5", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE6("moisture-soil-channel-6", (byte) 0x36, "Soil Moisture 6", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE7("moisture-soil-channel-7", (byte) 0x38, "Soil Moisture 7", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE8("moisture-soil-channel-8", (byte) 0x3A, "Soil Moisture 8", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE9("moisture-soil-channel-9", (byte) 0x3C, "Soil Moisture 9", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE10("moisture-soil-channel-10", (byte) 0x3E, "Soil Moisture 10", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE11("moisture-soil-channel-11", (byte) 0x40, "Soil Moisture 11", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE12("moisture-soil-channel-12", (byte) 0x42, "Soil Moisture 12", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE13("moisture-soil-channel-13", (byte) 0x44, "Soil Moisture 13", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE14("moisture-soil-channel-14", (byte) 0x46, "Soil Moisture 14", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE15("moisture-soil-channel-15", (byte) 0x48, "Soil Moisture 15", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
SOILMOISTURE16("moisture-soil-channel-16", (byte) 0x4A, "Soil Moisture 16", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
// will no longer be used
// skip battery-level, since it is read via Command.CMD_READ_SENSOR_ID_NEW
LOWBATT((byte) 0x4C, new Skip(1)),
PM25_24HAVG1("air-quality-24-hour-average-channel-1", (byte) 0x4D, "PM2.5 Air Quality 24 hour average channel 1",
MeasureType.PM25),
PM25_24HAVG2("air-quality-24-hour-average-channel-2", (byte) 0x4E, "PM2.5 Air Quality 24 hour average channel 2",
MeasureType.PM25),
PM25_24HAVG3("air-quality-24-hour-average-channel-3", (byte) 0x4F, "PM2.5 Air Quality 24 hour average channel 3",
MeasureType.PM25),
PM25_24HAVG4("air-quality-24-hour-average-channel-4", (byte) 0x50, "PM2.5 Air Quality 24 hour average channel 4",
MeasureType.PM25),
PM25_CH1("air-quality-channel-1", (byte) 0x2A, "PM2.5 Air Quality channel 1", MeasureType.PM25),
PM25_CH2("air-quality-channel-2", (byte) 0x51, "PM2.5 Air Quality channel 2", MeasureType.PM25),
PM25_CH3("air-quality-channel-3", (byte) 0x52, "PM2.5 Air Quality channel 3", MeasureType.PM25),
PM25_CH4("air-quality-channel-4", (byte) 0x53, "PM2.5 Air Quality channel 4", MeasureType.PM25),
LEAK_CH1("water-leak-channel-1", (byte) 0x58, "Leak channel 1", MeasureType.WATER_LEAK_DETECTION),
LEAK_CH2("water-leak-channel-2", (byte) 0x59, "Leak channel 2", MeasureType.WATER_LEAK_DETECTION),
LEAK_CH3("water-leak-channel-3", (byte) 0x5A, "Leak channel 3", MeasureType.WATER_LEAK_DETECTION),
LEAK_CH4("water-leak-channel-4", (byte) 0x5B, "Leak channel 4", MeasureType.WATER_LEAK_DETECTION),
// `LIGHTNING` is the name in the spec, so we keep it here as it
LIGHTNING("lightning-distance", (byte) 0x60, "lightning distance 1~40KM", MeasureType.LIGHTNING_DISTANCE),
LIGHTNING_TIME("lightning-time", (byte) 0x61, "lightning happened time", MeasureType.LIGHTNING_TIME),
// `LIGHTNING_POWER` is the name in the spec, so we keep it here as it
LIGHTNING_POWER("lightning-counter", (byte) 0x62, "lightning counter for the day", MeasureType.LIGHTNING_COUNTER),
TF_USR1("temperature-external-channel-1", (byte) 0x63, "Soil or Water temperature channel 1",
MeasureType.TEMPERATURE),
TF_USR2("temperature-external-channel-2", (byte) 0x64, "Soil or Water temperature channel 2",
MeasureType.TEMPERATURE),
TF_USR3("temperature-external-channel-3", (byte) 0x65, "Soil or Water temperature channel 3",
MeasureType.TEMPERATURE),
TF_USR4("temperature-external-channel-4", (byte) 0x66, "Soil or Water temperature channel 4",
MeasureType.TEMPERATURE),
TF_USR5("temperature-external-channel-5", (byte) 0x67, "Soil or Water temperature channel 5",
MeasureType.TEMPERATURE),
TF_USR6("temperature-external-channel-6", (byte) 0x68, "Soil or Water temperature channel 6",
MeasureType.TEMPERATURE),
TF_USR7("temperature-external-channel-7", (byte) 0x69, "Soil or Water temperature channel 7",
MeasureType.TEMPERATURE),
TF_USR8("temperature-external-channel-8", (byte) 0x6A, "Soil or Water temperature channel 8",
MeasureType.TEMPERATURE),
ITEM_SENSOR_CO2((byte) 0x70,
new MeasurandParser("sensor-co2-temperature", "Temperature (CO₂-Sensor)", MeasureType.TEMPERATURE),
new MeasurandParser("sensor-co2-humidity", "Humidity (CO₂-Sensor)", MeasureType.PERCENTAGE),
new MeasurandParser("sensor-co2-pm10", "PM10 Air Quality (CO₂-Sensor)", MeasureType.PM10),
new MeasurandParser("sensor-co2-pm10-24-hour-average", "PM10 Air Quality 24 hour average (CO₂-Sensor)",
MeasureType.PM10),
new MeasurandParser("sensor-co2-pm25", "PM2.5 Air Quality (CO₂-Sensor)", MeasureType.PM25),
new MeasurandParser("sensor-co2-pm25-24-hour-average", "PM2.5 Air Quality 24 hour average (CO₂-Sensor)",
MeasureType.PM25),
new MeasurandParser("sensor-co2-co2", "CO₂", MeasureType.CO2),
new MeasurandParser("sensor-co2-co2-24-hour-average", "CO₂ 24 hour average", MeasureType.CO2),
// skip battery-level, since it is read via Command.CMD_READ_SENSOR_ID_NEW
new Skip(1)),
ITEM_LEAF_WETNESS_CH1("leaf-wetness-channel-1", (byte) 0x72, "Leaf Moisture channel 1", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
ITEM_LEAF_WETNESS_CH2("leaf-wetness-channel-2", (byte) 0x73, "Leaf Moisture channel 2", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
ITEM_LEAF_WETNESS_CH3("leaf-wetness-channel-3", (byte) 0x74, "Leaf Moisture channel 3", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
ITEM_LEAF_WETNESS_CH4("leaf-wetness-channel-4", (byte) 0x75, "Leaf Moisture channel 4", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
ITEM_LEAF_WETNESS_CH5("leaf-wetness-channel-5", (byte) 0x76, "Leaf Moisture channel 5", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
ITEM_LEAF_WETNESS_CH6("leaf-wetness-channel-6", (byte) 0x77, "Leaf Moisture channel 6", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
ITEM_LEAF_WETNESS_CH7("leaf-wetness-channel-7", (byte) 0x78, "Leaf Moisture channel 7", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),
ITEM_LEAF_WETNESS_CH8("leaf-wetness-channel-8", (byte) 0x79, "Leaf Moisture channel 8", MeasureType.PERCENTAGE,
CHANNEL_TYPE_MOISTURE),;
private static final Map<Byte, Measurand> MEASURANDS = new HashMap<>();
static {
for (Measurand value : values()) {
MEASURANDS.put(value.code, value);
}
}
private final byte code;
private final Parser[] parsers;
Measurand(String channelId, byte code, String name, MeasureType measureType) {
this(channelId, code, name, measureType, null);
}
Measurand(String channelId, byte code, String name, MeasureType measureType,
@Nullable ChannelTypeUID channelTypeUID) {
this(code, new MeasurandParser(channelId, name, measureType, channelTypeUID));
}
Measurand(byte code, Parser... parsers) {
this.code = code;
this.parsers = parsers;
}
public static @Nullable Measurand getByCode(byte code) {
return MEASURANDS.get(code);
}
public int extractMeasuredValues(byte[] data, int offset, ConversionContext context, List<MeasuredValue> result) {
int subOffset = 0;
for (Parser parser : parsers) {
subOffset += parser.extractMeasuredValues(data, offset + subOffset, context, result);
}
return subOffset;
}
private interface Parser {
int extractMeasuredValues(byte[] data, int offset, ConversionContext context, List<MeasuredValue> result);
}
private static class Skip implements Parser {
private final int skip;
public Skip(int skip) {
this.skip = skip;
}
@Override
public int extractMeasuredValues(byte[] data, int offset, ConversionContext context,
List<MeasuredValue> result) {
return skip;
}
}
private static class MeasurandParser implements Parser {
private final String name;
private final String channelId;
private final MeasureType measureType;
private final @Nullable ChannelTypeUID channelTypeUID;
MeasurandParser(String channelId, String name, MeasureType measureType) {
this(channelId, name, measureType, null);
}
MeasurandParser(String channelId, String name, MeasureType measureType,
@Nullable ChannelTypeUID channelTypeUID) {
this.channelId = channelId;
this.name = name;
this.measureType = measureType;
this.channelTypeUID = channelTypeUID == null ? measureType.getChannelTypeId() : channelTypeUID;
}
public int extractMeasuredValues(byte[] data, int offset, ConversionContext context,
List<MeasuredValue> result) {
State state = measureType.toState(data, offset, context);
if (state != null) {
result.add(new MeasuredValue(measureType, channelId, channelTypeUID, state, name));
}
return measureType.getByteSize();
}
}
}

View File

@ -0,0 +1,171 @@
/**
* Copyright (c) 2010-2022 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.fineoffsetweatherstation.internal.domain;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.State;
/**
* Holds all the measurands supported by the gateway.
*
* @author Andreas Berger - Initial contribution
*/
@NonNullByDefault
public class Measurands {
private static final Map<Protocol, Measurands> INSTANCES = new HashMap<>();
private final Map<Byte, List<Parser>> parsersPerCode = new HashMap<>();
private Measurands(Protocol protocol) {
try (InputStream data = Measurands.class.getResourceAsStream("/measurands.csv")) {
if (data == null) {
throw new IllegalStateException("Missing measurands.csv");
}
CSVFormat csvFormat = CSVFormat.Builder.create().setHeader().setSkipHeaderRecord(true).build();
CSVParser.parse(new InputStreamReader(data), csvFormat).forEach(row -> {
byte code = Byte.valueOf(row.get("Code").replace("0x", ""), 16);
Optional<Integer> skip = Optional.ofNullable(row.get("Skip")).filter(Predicate.not(String::isBlank))
.map(Integer::valueOf);
int index = Optional.ofNullable(row.get("Index")).filter(Predicate.not(String::isBlank))
.map(Integer::valueOf).orElse(0);
Parser parser;
if (skip.isPresent()) {
parser = new Skip(skip.get(), index);
} else {
String name = row.get("Name");
String channel = row.get("Channel");
ChannelTypeUID channelType = Optional.ofNullable(row.get("ChannelType"))
.filter(Predicate.not(String::isBlank)).map(s -> {
if (s.contains(":")) {
return new ChannelTypeUID(s);
} else {
return new ChannelTypeUID(FineOffsetWeatherStationBindingConstants.BINDING_ID, s);
}
}).orElse(null);
String measurandString = protocol == Protocol.DEFAULT ? row.get("MeasureType_DEFAULT")
: Optional.ofNullable(row.get("MeasureType_" + protocol.name()))
.filter(Predicate.not(String::isBlank))
.orElseGet(() -> row.get("MeasureType_DEFAULT"));
parser = new MeasurandParser(channel, name, MeasureType.valueOf(measurandString), index,
channelType);
}
List<Parser> parsers = parsersPerCode.computeIfAbsent(code, aByte -> new ArrayList<>());
// noinspection ConstantConditions
if (parsers != null) {
parsers.add(parser);
}
});
for (List<Parser> parsers : parsersPerCode.values()) {
parsers.sort(Comparator.comparing(Parser::getIndex));
}
} catch (IOException e) {
throw new IllegalStateException("Failed to read measurands.csv", e);
}
}
public static Measurands getInstance(Protocol protocol) {
synchronized (INSTANCES) {
return Objects.requireNonNull(INSTANCES.computeIfAbsent(protocol, Measurands::new));
}
}
private abstract static class Parser {
private final int index;
public Parser(int index) {
this.index = index;
}
public abstract int extractMeasuredValues(byte[] data, int offset, ConversionContext context,
List<MeasuredValue> result);
public int getIndex() {
return index;
}
}
private static class Skip extends Parser {
private final int skip;
public Skip(int skip, int index) {
super(index);
this.skip = skip;
}
@Override
public int extractMeasuredValues(byte[] data, int offset, ConversionContext context,
List<MeasuredValue> result) {
return skip;
}
}
private static class MeasurandParser extends Parser {
private final String name;
private final String channelId;
private final MeasureType measureType;
private final @Nullable ChannelTypeUID channelTypeUID;
MeasurandParser(String channelId, String name, MeasureType measureType, int index,
@Nullable ChannelTypeUID channelTypeUID) {
super(index);
this.channelId = channelId;
this.name = name;
this.measureType = measureType;
this.channelTypeUID = channelTypeUID == null ? measureType.getChannelTypeId() : channelTypeUID;
}
public int extractMeasuredValues(byte[] data, int offset, ConversionContext context,
List<MeasuredValue> result) {
State state = measureType.toState(data, offset, context);
if (state != null) {
result.add(new MeasuredValue(measureType, channelId, channelTypeUID, state, name));
}
return measureType.getByteSize();
}
}
public int extractMeasuredValues(byte code, byte[] data, int offset, ConversionContext context,
List<MeasuredValue> result) {
List<Parser> parsers = parsersPerCode.get(code);
if (parsers == null) {
throw new IllegalArgumentException("No measurement for code 0x" + Integer.toHexString(code) + " defined");
}
int subOffset = 0;
for (Parser parser : parsers) {
subOffset += parser.extractMeasuredValues(data, offset + subOffset, context, result);
}
return subOffset;
}
}

View File

@ -36,6 +36,7 @@ import static org.openhab.binding.fineoffsetweatherstation.internal.Utils.toUInt
import static org.openhab.core.library.unit.SIUnits.CELSIUS; import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.SIUnits.METRE; import static org.openhab.core.library.unit.SIUnits.METRE;
import static org.openhab.core.library.unit.SIUnits.PASCAL; import static org.openhab.core.library.unit.SIUnits.PASCAL;
import static org.openhab.core.library.unit.SIUnits.SQUARE_METRE;
import static org.openhab.core.library.unit.Units.DEGREE_ANGLE; import static org.openhab.core.library.unit.Units.DEGREE_ANGLE;
import static org.openhab.core.library.unit.Units.METRE_PER_SECOND; import static org.openhab.core.library.unit.Units.METRE_PER_SECOND;
import static org.openhab.core.library.unit.Units.MICROGRAM_PER_CUBICMETRE; import static org.openhab.core.library.unit.Units.MICROGRAM_PER_CUBICMETRE;
@ -84,6 +85,8 @@ public enum MeasureType {
HEIGHT_PER_HOUR(MILLIMETRE_PER_HOUR, 2, CHANNEL_TYPE_RAIN_RATE, (data, offset) -> toUInt16(data, offset) / 10.), HEIGHT_PER_HOUR(MILLIMETRE_PER_HOUR, 2, CHANNEL_TYPE_RAIN_RATE, (data, offset) -> toUInt16(data, offset) / 10.),
HEIGHT_PER_HOUR_BIG(MILLIMETRE_PER_HOUR, 4, CHANNEL_TYPE_RAIN_RATE, (data, offset) -> toUInt32(data, offset) / 10.),
LUX(Units.LUX, 4, CHANNEL_TYPE_ILLUMINATION, (data, offset) -> toUInt32(data, offset) / 10.), LUX(Units.LUX, 4, CHANNEL_TYPE_ILLUMINATION, (data, offset) -> toUInt32(data, offset) / 10.),
PM25(MICROGRAM_PER_CUBICMETRE, 2, CHANNEL_TYPE_PM25, (data, offset) -> toUInt16(data, offset) / 10.), PM25(MICROGRAM_PER_CUBICMETRE, 2, CHANNEL_TYPE_PM25, (data, offset) -> toUInt16(data, offset) / 10.),
@ -104,8 +107,8 @@ public enum MeasureType {
(data, offset, context) -> new DateTimeType( (data, offset, context) -> new DateTimeType(
ZonedDateTime.ofInstant(Instant.ofEpochSecond(toUInt32(data, offset)), context.getZoneId()))), ZonedDateTime.ofInstant(Instant.ofEpochSecond(toUInt32(data, offset)), context.getZoneId()))),
MICROWATT_PER_SQUARE_CENTIMETRE(Units.MICROWATT_PER_SQUARE_CENTIMETRE, 2, CHANNEL_TYPE_UV_RADIATION, MILLIWATT_PER_SQUARE_METRE(MILLI(Units.WATT).divide(SQUARE_METRE), 2, CHANNEL_TYPE_UV_RADIATION,
Utils::toUInt16), (data, offset) -> Utils.toUInt16(data, offset) / 10.),
BYTE(1, null, (data, offset, context) -> new DecimalType(toUInt8(data[offset]))), BYTE(1, null, (data, offset, context) -> new DecimalType(toUInt8(data[offset]))),

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2010-2022 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.fineoffsetweatherstation.internal.domain;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetGatewayConfiguration;
import org.openhab.binding.fineoffsetweatherstation.internal.handler.ThingStatusListener;
import org.openhab.binding.fineoffsetweatherstation.internal.service.ELVGatewayQueryService;
import org.openhab.binding.fineoffsetweatherstation.internal.service.FineOffsetGatewayQueryService;
import org.openhab.binding.fineoffsetweatherstation.internal.service.GatewayQueryService;
/**
* The protocol defining the way the data is parsed
*
* @author Andreas Berger - Initial contribution
*/
@NonNullByDefault
public enum Protocol {
DEFAULT(FineOffsetGatewayQueryService::new),
ELV(ELVGatewayQueryService::new);
private final GatewayQueryServiceFactory queryServiceFactory;
Protocol(GatewayQueryServiceFactory queryServiceFactory) {
this.queryServiceFactory = queryServiceFactory;
}
public GatewayQueryService getGatewayQueryService(FineOffsetGatewayConfiguration config,
@Nullable ThingStatusListener thingStatusListener, ConversionContext conversionContext) {
return queryServiceFactory.newInstance(config, thingStatusListener, conversionContext);
}
private interface GatewayQueryServiceFactory {
GatewayQueryService newInstance(FineOffsetGatewayConfiguration config,
@Nullable ThingStatusListener thingStatusListener, ConversionContext conversionContext);
}
}

View File

@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -36,7 +37,7 @@ import org.openhab.binding.fineoffsetweatherstation.internal.domain.SensorGatewa
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SystemInfo; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SystemInfo;
import org.openhab.binding.fineoffsetweatherstation.internal.service.FineOffsetGatewayQueryService; import org.openhab.binding.fineoffsetweatherstation.internal.service.GatewayQueryService;
import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.i18n.TranslationProvider;
@ -56,6 +57,7 @@ import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeUID; import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State; import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.framework.Bundle; import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil; import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -76,7 +78,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
private final Bundle bundle; private final Bundle bundle;
private final ConversionContext conversionContext; private final ConversionContext conversionContext;
private @Nullable FineOffsetGatewayQueryService gatewayQueryService; private @Nullable GatewayQueryService gatewayQueryService;
private final FineOffsetGatewayDiscoveryService gatewayDiscoveryService; private final FineOffsetGatewayDiscoveryService gatewayDiscoveryService;
private final ChannelTypeRegistry channelTypeRegistry; private final ChannelTypeRegistry channelTypeRegistry;
@ -94,7 +96,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
ChannelTypeRegistry channelTypeRegistry, TranslationProvider translationProvider, ChannelTypeRegistry channelTypeRegistry, TranslationProvider translationProvider,
LocaleProvider localeProvider, TimeZoneProvider timeZoneProvider) { LocaleProvider localeProvider, TimeZoneProvider timeZoneProvider) {
super(bridge); super(bridge);
bridgeUID = bridge.getUID(); this.bridgeUID = bridge.getUID();
this.gatewayDiscoveryService = gatewayDiscoveryService; this.gatewayDiscoveryService = gatewayDiscoveryService;
this.channelTypeRegistry = channelTypeRegistry; this.channelTypeRegistry = channelTypeRegistry;
this.translationProvider = translationProvider; this.translationProvider = translationProvider;
@ -110,7 +112,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
@Override @Override
public void initialize() { public void initialize() {
FineOffsetGatewayConfiguration config = getConfigAs(FineOffsetGatewayConfiguration.class); FineOffsetGatewayConfiguration config = getConfigAs(FineOffsetGatewayConfiguration.class);
gatewayQueryService = new FineOffsetGatewayQueryService(config, this::updateStatus, conversionContext); gatewayQueryService = config.protocol.getGatewayQueryService(config, this::updateStatus, conversionContext);
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
fetchAndUpdateSensors(); fetchAndUpdateSensors();
@ -122,7 +124,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
private void fetchAndUpdateSensors() { private void fetchAndUpdateSensors() {
@Nullable @Nullable
Map<SensorGatewayBinding, SensorDevice> deviceMap = query(FineOffsetGatewayQueryService::getRegisteredSensors); Map<SensorGatewayBinding, SensorDevice> deviceMap = query(GatewayQueryService::getRegisteredSensors);
sensorDeviceMap = deviceMap; sensorDeviceMap = deviceMap;
updateSensors(); updateSensors();
if (deviceMap != null) { if (deviceMap != null) {
@ -154,8 +156,9 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
if (disposed) { if (disposed) {
return; return;
} }
List<MeasuredValue> data = query(FineOffsetGatewayQueryService::getLiveData); List<MeasuredValue> data = query(GatewayQueryService::getMeasuredValues);
if (data == null) { if (data == null) {
getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.UNDEF));
return; return;
} }
@ -174,7 +177,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
} }
} }
if (!channels.isEmpty()) { if (!channels.isEmpty()) {
updateThing(editThing().withChannels(channels).build()); updateBridgeThing(bridgeBuilder -> bridgeBuilder.withChannels(channels));
} }
} }
@ -208,27 +211,31 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
private void updateBridgeInfo() { private void updateBridgeInfo() {
@Nullable @Nullable
String firmware = query(FineOffsetGatewayQueryService::getFirmwareVersion); String firmware = query(GatewayQueryService::getFirmwareVersion);
Map<String, String> properties = new HashMap<>(thing.getProperties()); Map<String, String> properties = new HashMap<>(thing.getProperties());
if (firmware != null) { if (firmware != null) {
var fwString = firmware.split("_V"); var fwString = firmware.split("_?V");
if (fwString.length > 1) { if (fwString.length > 1) {
properties.put(Thing.PROPERTY_MODEL_ID, fwString[0]); properties.put(Thing.PROPERTY_MODEL_ID, fwString[0]);
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, fwString[1]); properties.put(Thing.PROPERTY_FIRMWARE_VERSION, fwString[1]);
} }
} }
SystemInfo systemInfo = query(FineOffsetGatewayQueryService::fetchSystemInfo); SystemInfo systemInfo = query(GatewayQueryService::fetchSystemInfo);
if (systemInfo != null && systemInfo.getFrequency() != null) { if (systemInfo != null && systemInfo.getFrequency() != null) {
properties.put(PROPERTY_FREQUENCY, systemInfo.getFrequency() + " MHz"); properties.put(PROPERTY_FREQUENCY, systemInfo.getFrequency() + " MHz");
} }
if (!thing.getProperties().equals(properties)) { if (!thing.getProperties().equals(properties)) {
BridgeBuilder bridge = editThing(); updateBridgeThing(bridgeBuilder -> bridgeBuilder.withProperties(properties));
bridge.withProperties(properties);
updateThing(bridge.build());
} }
} }
private void updateBridgeThing(Consumer<BridgeBuilder> customizer) {
BridgeBuilder bridge = editThing();
customizer.accept(bridge);
updateThing(bridge.build());
}
private void startDiscoverJob() { private void startDiscoverJob() {
ScheduledFuture<?> job = discoverJob; ScheduledFuture<?> job = discoverJob;
if (job == null || job.isCancelled()) { if (job == null || job.isCancelled()) {
@ -262,9 +269,9 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
this.pollingJob = null; this.pollingJob = null;
} }
private <T> @Nullable T query(Function<FineOffsetGatewayQueryService, T> delegate) { private <T> @Nullable T query(Function<GatewayQueryService, T> delegate) {
@Nullable @Nullable
FineOffsetGatewayQueryService queryService = this.gatewayQueryService; GatewayQueryService queryService = this.gatewayQueryService;
if (queryService == null) { if (queryService == null) {
return null; return null;
} }
@ -275,7 +282,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler {
public void dispose() { public void dispose() {
disposed = true; disposed = true;
@Nullable @Nullable
FineOffsetGatewayQueryService queryService = this.gatewayQueryService; GatewayQueryService queryService = this.gatewayQueryService;
if (queryService != null) { if (queryService != null) {
try { try {
queryService.close(); queryService.close();

View File

@ -27,6 +27,7 @@ import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.UnDefType;
/** /**
* The {@link FineOffsetSensorHandler} keeps track of the signal and battery of the sensor attached to the gateway. * The {@link FineOffsetSensorHandler} keeps track of the signal and battery of the sensor attached to the gateway.
@ -61,7 +62,10 @@ public class FineOffsetSensorHandler extends BaseThingHandler {
return; return;
} }
if (sensorDevice == null) { if (sensorDevice == null) {
updateStatus(ThingStatus.OFFLINE); // this only happens, if sensor data was read out correctly from the gateway, but the things' device
// (sensor) is no longer part of the paired sensors
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE);
getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.UNDEF));
return; return;
} }
if (sensorDevice.getSignal() == 0) { if (sensorDevice.getSignal() == 0) {

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2022 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.fineoffsetweatherstation.internal.service;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetGatewayConfiguration;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Command;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Protocol;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.SensorGatewayBinding;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SystemInfo;
import org.openhab.binding.fineoffsetweatherstation.internal.handler.ThingStatusListener;
/**
* Service to query an ELV gateway device.
*
* @author Andreas Berger - Initial contribution
*/
@NonNullByDefault
public class ELVGatewayQueryService extends GatewayQueryService {
private final FineOffsetDataParser fineOffsetDataParser;
private final ConversionContext conversionContext;
public ELVGatewayQueryService(FineOffsetGatewayConfiguration config,
@Nullable ThingStatusListener thingStatusListener, ConversionContext conversionContext) {
super(config, thingStatusListener);
this.fineOffsetDataParser = new FineOffsetDataParser(Protocol.ELV);
this.conversionContext = conversionContext;
}
@Override
public @Nullable String getFirmwareVersion() {
Command command = Command.CMD_READ_FIRMWARE_VERSION;
var data = executeCommand(command.name(), command.getPayloadAlternative(), bytes -> true);
if (null != data) {
return fineOffsetDataParser.getFirmwareVersion(data);
}
return null;
}
@Override
public Map<SensorGatewayBinding, SensorDevice> getRegisteredSensors() {
// not supported by ELV device
return Collections.emptyMap();
}
@Override
public @Nullable SystemInfo fetchSystemInfo() {
// not supported by ELV device
return null;
}
@Override
public List<MeasuredValue> getMeasuredValues() {
Command command = Command.CMD_WS980_LIVEDATA;
// since this request has 2 checksums we shortcut it here and provide the concrete payload directly
byte[] payload = new byte[] { (byte) 0xff, (byte) 0xff, (byte) 0x0b, (byte) 0x00, (byte) 0x06, (byte) 0x04,
(byte) 0x04, (byte) 0x19 };
byte[] data = executeCommand(command.name(), payload, command::isResponseValid);
if (data == null) {
return Collections.emptyList();
}
return fineOffsetDataParser.getMeasuredValues(data, conversionContext);
}
}

View File

@ -27,7 +27,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.Utils; import org.openhab.binding.fineoffsetweatherstation.internal.Utils;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext; import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Measurand; import org.openhab.binding.fineoffsetweatherstation.internal.domain.Measurands;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Protocol;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.SensorGatewayBinding; import org.openhab.binding.fineoffsetweatherstation.internal.domain.SensorGatewayBinding;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.BatteryStatus; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.BatteryStatus;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
@ -44,6 +45,11 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class FineOffsetDataParser { public class FineOffsetDataParser {
private final Logger logger = LoggerFactory.getLogger(FineOffsetDataParser.class); private final Logger logger = LoggerFactory.getLogger(FineOffsetDataParser.class);
private final Protocol protocol;
public FineOffsetDataParser(Protocol protocol) {
this.protocol = protocol;
}
public @Nullable String getFirmwareVersion(byte[] data) { public @Nullable String getFirmwareVersion(byte[] data) {
if (data.length > 0) { if (data.length > 0) {
@ -145,7 +151,7 @@ public class FineOffsetDataParser {
return new SystemInfo(frequency, date, dst, useWh24); return new SystemInfo(frequency, date, dst, useWh24);
} }
List<MeasuredValue> getLiveData(byte[] data, ConversionContext context) { List<MeasuredValue> getMeasuredValues(byte[] data, ConversionContext context) {
/* /*
* Pos| Length | Description * Pos| Length | Description
* ------------------------------------------------- * -------------------------------------------------
@ -165,16 +171,20 @@ public class FineOffsetDataParser {
* | 1 | checksum * | 1 | checksum
*/ */
var idx = 5; var idx = 5;
if (protocol == Protocol.ELV) {
idx++; // at index 5 there is an additional Byte being set to 0x04
}
var size = toUInt16(data, 3); var size = toUInt16(data, 3);
List<MeasuredValue> result = new ArrayList<>(); List<MeasuredValue> result = new ArrayList<>();
Measurands measurands = Measurands.getInstance(protocol);
while (idx < size) { while (idx < size) {
byte code = data[idx++]; byte code = data[idx++];
Measurand measurand = Measurand.getByCode(code); try {
if (measurand == null) { idx += measurands.extractMeasuredValues(code, data, idx, context, result);
logger.warn("failed to get measurand 0x{}", Integer.toHexString(code)); } catch (IllegalArgumentException e) {
logger.warn("", e);
return result; return result;
} }
idx += measurand.extractMeasuredValues(data, idx, context, result);
} }
return result; return result;
} }

View File

@ -12,10 +12,6 @@
*/ */
package org.openhab.binding.fineoffsetweatherstation.internal.service; package org.openhab.binding.fineoffsetweatherstation.internal.service;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -23,16 +19,14 @@ import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetGatewayConfiguration; import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetGatewayConfiguration;
import org.openhab.binding.fineoffsetweatherstation.internal.Utils;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Command; import org.openhab.binding.fineoffsetweatherstation.internal.domain.Command;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext; import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Protocol;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.SensorGatewayBinding; import org.openhab.binding.fineoffsetweatherstation.internal.domain.SensorGatewayBinding;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SystemInfo; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SystemInfo;
import org.openhab.binding.fineoffsetweatherstation.internal.handler.ThingStatusListener; import org.openhab.binding.fineoffsetweatherstation.internal.handler.ThingStatusListener;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -42,24 +36,21 @@ import org.slf4j.LoggerFactory;
* @author Andreas Berger - Initial contribution * @author Andreas Berger - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class FineOffsetGatewayQueryService implements AutoCloseable { public class FineOffsetGatewayQueryService extends GatewayQueryService {
private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayQueryService.class); private final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayQueryService.class);
private @Nullable Socket socket;
private final FineOffsetGatewayConfiguration config;
private final ThingStatusListener thingStatusListener;
private final FineOffsetDataParser fineOffsetDataParser; private final FineOffsetDataParser fineOffsetDataParser;
private final ConversionContext conversionContext; private final ConversionContext conversionContext;
public FineOffsetGatewayQueryService(FineOffsetGatewayConfiguration config, ThingStatusListener thingStatusListener, public FineOffsetGatewayQueryService(FineOffsetGatewayConfiguration config,
ConversionContext conversionContext) { @Nullable ThingStatusListener thingStatusListener, ConversionContext conversionContext) {
this.config = config; super(config, thingStatusListener);
this.thingStatusListener = thingStatusListener; this.fineOffsetDataParser = new FineOffsetDataParser(Protocol.DEFAULT);
this.fineOffsetDataParser = new FineOffsetDataParser();
this.conversionContext = conversionContext; this.conversionContext = conversionContext;
} }
@Override
public @Nullable String getFirmwareVersion() { public @Nullable String getFirmwareVersion() {
var data = executeCommand(Command.CMD_READ_FIRMWARE_VERSION); var data = executeCommand(Command.CMD_READ_FIRMWARE_VERSION);
if (null != data) { if (null != data) {
@ -68,6 +59,7 @@ public class FineOffsetGatewayQueryService implements AutoCloseable {
return null; return null;
} }
@Override
public Map<SensorGatewayBinding, SensorDevice> getRegisteredSensors() { public Map<SensorGatewayBinding, SensorDevice> getRegisteredSensors() {
var data = executeCommand(Command.CMD_READ_SENSOR_ID_NEW); var data = executeCommand(Command.CMD_READ_SENSOR_ID_NEW);
if (null == data) { if (null == data) {
@ -83,6 +75,7 @@ public class FineOffsetGatewayQueryService implements AutoCloseable {
}); });
} }
@Override
public @Nullable SystemInfo fetchSystemInfo() { public @Nullable SystemInfo fetchSystemInfo() {
var data = executeCommand(Command.CMD_READ_SSSS); var data = executeCommand(Command.CMD_READ_SSSS);
if (data == null) { if (data == null) {
@ -92,80 +85,16 @@ public class FineOffsetGatewayQueryService implements AutoCloseable {
return fineOffsetDataParser.fetchSystemInfo(data); return fineOffsetDataParser.fetchSystemInfo(data);
} }
public List<MeasuredValue> getLiveData() { @Override
public List<MeasuredValue> getMeasuredValues() {
byte[] data = executeCommand(Command.CMD_GW1000_LIVEDATA); byte[] data = executeCommand(Command.CMD_GW1000_LIVEDATA);
if (data == null) { if (data == null) {
return Collections.emptyList(); return Collections.emptyList();
} }
return fineOffsetDataParser.getLiveData(data, conversionContext); return fineOffsetDataParser.getMeasuredValues(data, conversionContext);
} }
private synchronized byte @Nullable [] executeCommand(Command command) { protected byte @Nullable [] executeCommand(Command command) {
byte[] buffer = new byte[2028]; return executeCommand(command.name(), command.getPayload(), command::isResponseValid);
int bytesRead;
byte[] request = command.getPayload();
try {
Socket socket = getConnection();
if (socket == null) {
return null;
}
InputStream in = socket.getInputStream();
socket.getOutputStream().write(request);
if ((bytesRead = in.read(buffer)) == -1) {
return null;
}
if (!command.isResponseValid(buffer)) {
if (bytesRead > 0) {
logger.debug("executeCommand({}), invalid response: {}", command,
Utils.toHexString(buffer, bytesRead, ""));
} else {
logger.debug("executeCommand({}): no response", command);
}
return null;
}
} catch (IOException ex) {
thingStatusListener.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
ex.getMessage());
try {
close();
} catch (IOException e) {
// ignored
}
return null;
} catch (Exception ex) {
logger.warn("executeCommand({})", command, ex);
return null;
}
var data = Arrays.copyOfRange(buffer, 0, bytesRead);
logger.trace("executeCommand({}): received: {}", command, Utils.toHexString(data, data.length, ""));
return data;
}
private synchronized @Nullable Socket getConnection() {
Socket socket = this.socket;
if (socket == null) {
try {
socket = new Socket(config.ip, config.port);
socket.setSoTimeout(5000);
this.socket = socket;
thingStatusListener.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
} catch (IOException e) {
thingStatusListener.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
e.getMessage());
}
}
return socket;
}
@Override
public void close() throws IOException {
Socket socket = this.socket;
this.socket = null;
if (socket != null) {
socket.close();
}
} }
} }

View File

@ -0,0 +1,161 @@
/**
* Copyright (c) 2010-2022 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.fineoffsetweatherstation.internal.service;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetGatewayConfiguration;
import org.openhab.binding.fineoffsetweatherstation.internal.Utils;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.SensorGatewayBinding;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SensorDevice;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.SystemInfo;
import org.openhab.binding.fineoffsetweatherstation.internal.handler.ThingStatusListener;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Interface defining the API for querying a gateway device.
*
* @author Andreas Berger - Initial contribution
*/
@NonNullByDefault
public abstract class GatewayQueryService implements AutoCloseable {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final Lock REQUEST_LOCK = new ReentrantLock();
private @Nullable Socket socket;
@Nullable
private final ThingStatusListener thingStatusListener;
private final FineOffsetGatewayConfiguration config;
@Nullable
public abstract String getFirmwareVersion();
public abstract Map<SensorGatewayBinding, SensorDevice> getRegisteredSensors();
@Nullable
public abstract SystemInfo fetchSystemInfo();
public abstract List<MeasuredValue> getMeasuredValues();
public GatewayQueryService(FineOffsetGatewayConfiguration config,
@Nullable ThingStatusListener thingStatusListener) {
this.config = config;
this.thingStatusListener = thingStatusListener;
}
protected byte @Nullable [] executeCommand(String command, byte[] request,
Function<byte[], Boolean> validateResponse) {
byte[] buffer = new byte[2028];
int bytesRead;
try {
if (!REQUEST_LOCK.tryLock(30, TimeUnit.SECONDS)) {
logger.trace("executeCommand({}): time out while getting lock", command);
return null;
}
Socket socket = getConnection();
if (socket == null) {
return null;
}
logger.trace("executeCommand({}): send request: {}", command,
Utils.toHexString(request, request.length, ""));
InputStream in = socket.getInputStream();
socket.getOutputStream().write(request);
if ((bytesRead = in.read(buffer)) == -1) {
logger.trace("executeCommand({}): data exceeded buffer length ({})", command, buffer.length);
return null;
}
if (!validateResponse.apply(buffer)) {
if (bytesRead > 0) {
logger.debug("executeCommand({}), invalid response: {}", command,
Utils.toHexString(buffer, bytesRead, ""));
} else {
logger.debug("executeCommand({}): no response", command);
}
return null;
}
} catch (IOException ex) {
@Nullable
ThingStatusListener statusListener = thingStatusListener;
if (statusListener != null) {
statusListener.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
ex.getMessage());
}
try {
close();
} catch (IOException e) {
// ignored
}
return null;
} catch (Exception ex) {
logger.warn("executeCommand({})", command, ex);
return null;
} finally {
REQUEST_LOCK.unlock();
}
var data = Arrays.copyOfRange(buffer, 0, bytesRead);
logger.trace("executeCommand({}): received: {}", command, Utils.toHexString(data, data.length, ""));
return data;
}
protected synchronized @Nullable Socket getConnection() {
Socket socket = this.socket;
if (socket == null) {
@Nullable
ThingStatusListener statusListener = thingStatusListener;
try {
socket = new Socket(config.ip, config.port);
socket.setSoTimeout(5000);
this.socket = socket;
if (statusListener != null) {
statusListener.updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
}
} catch (IOException e) {
if (statusListener != null) {
statusListener.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
e.getMessage());
}
}
}
return socket;
}
@Override
public void close() throws IOException {
Socket socket = this.socket;
this.socket = null;
if (socket != null) {
socket.close();
}
}
}

View File

@ -16,6 +16,11 @@
<description>The network port of the gateway</description> <description>The network port of the gateway</description>
<default>45000</default> <default>45000</default>
</parameter> </parameter>
<parameter name="protocol" type="text" required="false">
<label>Protocol</label>
<description>The protocol to use for communicating with the gateway, valid values are: `DEFAULT` or `ELV`</description>
<default>DEFAULT</default>
</parameter>
<parameter name="pollingInterval" type="integer" required="true"> <parameter name="pollingInterval" type="integer" required="true">
<label>Polling Interval</label> <label>Polling Interval</label>
<description>Polling interval for refreshing the data in seconds</description> <description>Polling interval for refreshing the data in seconds</description>

View File

@ -20,6 +20,8 @@ thing-type.config.fineoffsetweatherstation.gateway.pollingInterval.label = Polli
thing-type.config.fineoffsetweatherstation.gateway.pollingInterval.description = Polling interval for refreshing the data in seconds thing-type.config.fineoffsetweatherstation.gateway.pollingInterval.description = Polling interval for refreshing the data in seconds
thing-type.config.fineoffsetweatherstation.gateway.port.label = Port thing-type.config.fineoffsetweatherstation.gateway.port.label = Port
thing-type.config.fineoffsetweatherstation.gateway.port.description = The network port of the gateway thing-type.config.fineoffsetweatherstation.gateway.port.description = The network port of the gateway
thing-type.config.fineoffsetweatherstation.gateway.protocol.label = Protocol
thing-type.config.fineoffsetweatherstation.gateway.protocol.description = The protocol to use for communicating with the gateway, valid values are: `DEFAULT` or `ELV`
thing-type.config.fineoffsetweatherstation.sensor.sensor.label = Sensor thing-type.config.fineoffsetweatherstation.sensor.sensor.label = Sensor
# channel types # channel types

View File

@ -8,6 +8,7 @@
<label>Gateway Device</label> <label>Gateway Device</label>
<description>A WiFi connected gateway device (WN1900, GW1000, GW1100, WH2680, WH2650) to bridge Sensors</description> <description>A WiFi connected gateway device (WN1900, GW1000, GW1100, WH2680, WH2650) to bridge Sensors</description>
<category>NetworkAppliance</category> <category>NetworkAppliance</category>
<representation-property>macAddress</representation-property>
<config-description-ref uri="thing-type:fineoffsetweatherstation:gateway"/> <config-description-ref uri="thing-type:fineoffsetweatherstation:gateway"/>
</bridge-type> </bridge-type>

View File

@ -19,6 +19,7 @@
<channel id="lowBattery" typeId="system.low-battery"/> <channel id="lowBattery" typeId="system.low-battery"/>
</channels> </channels>
<representation-property>sensor</representation-property>
<config-description-ref uri="thing-type:fineoffsetweatherstation:sensor"/> <config-description-ref uri="thing-type:fineoffsetweatherstation:sensor"/>
</thing-type> </thing-type>
</thing:thing-descriptions> </thing:thing-descriptions>

View File

@ -0,0 +1,115 @@
ManufacturerName,Name,Channel,Code,Index,Skip,ChannelType,MeasureType_DEFAULT,MeasureType_ELV
INTEMP,Indoor Temperature,temperature-indoor,0x1,,,system:indoor-temperature,TEMPERATURE,
OUTTEMP,Outdoor Temperature,temperature-outdoor,0x2,,,system:outdoor-temperature,TEMPERATURE,
DEWPOINT,Dew point,temperature-dew-point,0x3,,,,TEMPERATURE,
WINDCHILL,Wind chill,temperature-wind-chill,0x4,,,,TEMPERATURE,
HEATINDEX,Heat index,temperature-heat-index,0x5,,,,TEMPERATURE,
INHUMI,Indoor Humidity,humidity-indoor,0x6,,,,PERCENTAGE,
OUTHUMI,Outdoor Humidity,humidity-outdoor,0x7,,,system:atmospheric-humidity,PERCENTAGE,
ABSBARO,Absolutely pressure,pressure-absolute,0x8,,,,PRESSURE,
RELBARO,Relative pressure,pressure-relative,0x9,,,system:barometric-pressure,PRESSURE,
WINDDIRECTION,Wind Direction,direction-wind,0xa,,,system:wind-direction,DEGREE,
WINDSPEED,Wind Speed,speed-wind,0xb,,,system:wind-speed,SPEED,
GUSTSPEED,Gust Speed,speed-gust,0xc,,,system:wind-speed,SPEED,
RAINEVENT,Rain Event,rain-event,0xd,,,,HEIGHT,HEIGHT_BIG
RAINRATE,Rain Rate,rain-rate,0xe,,,,HEIGHT_PER_HOUR,HEIGHT_PER_HOUR_BIG
RAINHOUR,Rain hour,rain-hour,0xf,,,,HEIGHT,HEIGHT_BIG
RAINDAY,Rain Day,rain-day,0x10,,,,HEIGHT,HEIGHT_BIG
RAINWEEK,Rain Week,rain-week,0x11,,,,HEIGHT,HEIGHT_BIG
RAINMONTH,Rain Month,rain-month,0x12,,,,HEIGHT_BIG,
RAINYEAR,Rain Year,rain-year,0x13,,,,HEIGHT_BIG,
RAINTOTALS,Rain Totals,rain-total,0x14,,,,HEIGHT_BIG,
LIGHT,Light,illumination,0x15,,,,LUX,
UV,UV,irradiation-uv,0x16,,,,MILLIWATT_PER_SQUARE_METRE,
UVI,UV index,uv-index,0x17,,,uv-index,BYTE,
TIME,Date and time,time,0x18,,,,DATE_TIME2,
DAYLWINDMAX,Day max wind,wind-max-day,0x19,,,max-wind-speed,SPEED,
TEMP1,Temperature 1,temperature-channel-1,0x1a,,,,TEMPERATURE,
TEMP2,Temperature 2,temperature-channel-2,0x1b,,,,TEMPERATURE,
TEMP3,Temperature 3,temperature-channel-3,0x1c,,,,TEMPERATURE,
TEMP4,Temperature 4,temperature-channel-4,0x1d,,,,TEMPERATURE,
TEMP5,Temperature 5,temperature-channel-5,0x1e,,,,TEMPERATURE,
TEMP6,Temperature 6,temperature-channel-6,0x1f,,,,TEMPERATURE,
TEMP7,Temperature 7,temperature-channel-7,0x20,,,,TEMPERATURE,
TEMP8,Temperature 8,temperature-channel-8,0x21,,,,TEMPERATURE,
HUMI1,Humidity 1,humidity-channel-1,0x22,,,,PERCENTAGE,
HUMI2,Humidity 2,humidity-channel-2,0x23,,,,PERCENTAGE,
HUMI3,Humidity 3,humidity-channel-3,0x24,,,,PERCENTAGE,
HUMI4,Humidity 4,humidity-channel-4,0x25,,,,PERCENTAGE,
HUMI5,Humidity 5,humidity-channel-5,0x26,,,,PERCENTAGE,
HUMI6,Humidity 6,humidity-channel-6,0x27,,,,PERCENTAGE,
HUMI7,Humidity 7,humidity-channel-7,0x28,,,,PERCENTAGE,
HUMI8,Humidity 8,humidity-channel-8,0x29,,,,PERCENTAGE,
SOILTEMP1,Soil Temperature 1,temperature-soil-channel-1,0x2b,,,,TEMPERATURE,
SOILTEMP2,Soil Temperature 2,temperature-soil-channel-2,0x2d,,,,TEMPERATURE,
SOILTEMP3,Soil Temperature 3,temperature-soil-channel-3,0x2f,,,,TEMPERATURE,
SOILTEMP4,Soil Temperature 4,temperature-soil-channel-4,0x31,,,,TEMPERATURE,
SOILTEMP5,Soil Temperature 5,temperature-soil-channel-5,0x33,,,,TEMPERATURE,
SOILTEMP6,Soil Temperature 6,temperature-soil-channel-6,0x35,,,,TEMPERATURE,
SOILTEMP7,Soil Temperature 7,temperature-soil-channel-7,0x37,,,,TEMPERATURE,
SOILTEMP8,Soil Temperature 8,temperature-soil-channel-8,0x39,,,,TEMPERATURE,
SOILTEMP9,Soil Temperature 9,temperature-soil-channel-9,0x3b,,,,TEMPERATURE,
SOILTEMP10,Soil Temperature 10,temperature-soil-channel-10,0x3d,,,,TEMPERATURE,
SOILTEMP11,Soil Temperature 11,temperature-soil-channel-11,0x3f,,,,TEMPERATURE,
SOILTEMP12,Soil Temperature 12,temperature-soil-channel-12,0x41,,,,TEMPERATURE,
SOILTEMP13,Soil Temperature 13,temperature-soil-channel-13,0x43,,,,TEMPERATURE,
SOILTEMP14,Soil Temperature 14,temperature-soil-channel-14,0x45,,,,TEMPERATURE,
SOILTEMP15,Soil Temperature 15,temperature-soil-channel-15,0x47,,,,TEMPERATURE,
SOILTEMP16,Soil Temperature 16,temperature-soil-channel-16,0x49,,,,TEMPERATURE,
SOILMOISTURE1,Soil Moisture 1,moisture-soil-channel-1,0x2c,,,moisture,PERCENTAGE,
SOILMOISTURE2,Soil Moisture 2,moisture-soil-channel-2,0x2e,,,moisture,PERCENTAGE,
SOILMOISTURE3,Soil Moisture 3,moisture-soil-channel-3,0x30,,,moisture,PERCENTAGE,
SOILMOISTURE4,Soil Moisture 4,moisture-soil-channel-4,0x32,,,moisture,PERCENTAGE,
SOILMOISTURE5,Soil Moisture 5,moisture-soil-channel-5,0x34,,,moisture,PERCENTAGE,
SOILMOISTURE6,Soil Moisture 6,moisture-soil-channel-6,0x36,,,moisture,PERCENTAGE,
SOILMOISTURE7,Soil Moisture 7,moisture-soil-channel-7,0x38,,,moisture,PERCENTAGE,
SOILMOISTURE8,Soil Moisture 8,moisture-soil-channel-8,0x3a,,,moisture,PERCENTAGE,
SOILMOISTURE9,Soil Moisture 9,moisture-soil-channel-9,0x3c,,,moisture,PERCENTAGE,
SOILMOISTURE10,Soil Moisture 10,moisture-soil-channel-10,0x3e,,,moisture,PERCENTAGE,
SOILMOISTURE11,Soil Moisture 11,moisture-soil-channel-11,0x40,,,moisture,PERCENTAGE,
SOILMOISTURE12,Soil Moisture 12,moisture-soil-channel-12,0x42,,,moisture,PERCENTAGE,
SOILMOISTURE13,Soil Moisture 13,moisture-soil-channel-13,0x44,,,moisture,PERCENTAGE,
SOILMOISTURE14,Soil Moisture 14,moisture-soil-channel-14,0x46,,,moisture,PERCENTAGE,
SOILMOISTURE15,Soil Moisture 15,moisture-soil-channel-15,0x48,,,moisture,PERCENTAGE,
SOILMOISTURE16,Soil Moisture 16,moisture-soil-channel-16,0x4a,,,moisture,PERCENTAGE,
LOWBATT,Low Battery,,0x4c,,1,,,
PM25_24HAVG1,PM2.5 Air Quality 24 hour average channel 1,air-quality-24-hour-average-channel-1,0x4d,,,,PM25,
PM25_24HAVG2,PM2.5 Air Quality 24 hour average channel 2,air-quality-24-hour-average-channel-2,0x4e,,,,PM25,
PM25_24HAVG3,PM2.5 Air Quality 24 hour average channel 3,air-quality-24-hour-average-channel-3,0x4f,,,,PM25,
PM25_24HAVG4,PM2.5 Air Quality 24 hour average channel 4,air-quality-24-hour-average-channel-4,0x50,,,,PM25,
PM25_CH1,PM2.5 Air Quality channel 1,air-quality-channel-1,0x2a,,,,PM25,
PM25_CH2,PM2.5 Air Quality channel 2,air-quality-channel-2,0x51,,,,PM25,
PM25_CH3,PM2.5 Air Quality channel 3,air-quality-channel-3,0x52,,,,PM25,
PM25_CH4,PM2.5 Air Quality channel 4,air-quality-channel-4,0x53,,,,PM25,
LEAK_CH1,Leak channel 1,water-leak-channel-1,0x58,,,,WATER_LEAK_DETECTION,
LEAK_CH2,Leak channel 2,water-leak-channel-2,0x59,,,,WATER_LEAK_DETECTION,
LEAK_CH3,Leak channel 3,water-leak-channel-3,0x5a,,,,WATER_LEAK_DETECTION,
LEAK_CH4,Leak channel 4,water-leak-channel-4,0x5b,,,,WATER_LEAK_DETECTION,
LIGHTNING,lightning distance 1~40KM,lightning-distance,0x60,,,,LIGHTNING_DISTANCE,
LIGHTNING_TIME,lightning happened time,lightning-time,0x61,,,,LIGHTNING_TIME,
LIGHTNING_POWER,lightning counter for the day,lightning-counter,0x62,,,,LIGHTNING_COUNTER,
TF_USR1,Soil or Water temperature channel 1,temperature-external-channel-1,0x63,,,,TEMPERATURE,
TF_USR2,Soil or Water temperature channel 2,temperature-external-channel-2,0x64,,,,TEMPERATURE,
TF_USR3,Soil or Water temperature channel 3,temperature-external-channel-3,0x65,,,,TEMPERATURE,
TF_USR4,Soil or Water temperature channel 4,temperature-external-channel-4,0x66,,,,TEMPERATURE,
TF_USR5,Soil or Water temperature channel 5,temperature-external-channel-5,0x67,,,,TEMPERATURE,
TF_USR6,Soil or Water temperature channel 6,temperature-external-channel-6,0x68,,,,TEMPERATURE,
TF_USR7,Soil or Water temperature channel 7,temperature-external-channel-7,0x69,,,,TEMPERATURE,
TF_USR8,Soil or Water temperature channel 8,temperature-external-channel-8,0x6a,,,,TEMPERATURE,
ITEM_SENSOR_CO2,Temperature (CO₂-Sensor),sensor-co2-temperature,0x70,0,,,TEMPERATURE,
ITEM_SENSOR_CO2,Humidity (CO₂-Sensor),sensor-co2-humidity,0x70,1,,,PERCENTAGE,
ITEM_SENSOR_CO2,PM10 Air Quality (CO₂-Sensor),sensor-co2-pm10,0x70,2,,,PM10,
ITEM_SENSOR_CO2,PM10 Air Quality 24 hour average (CO₂-Sensor),sensor-co2-pm10-24-hour-average,0x70,3,,,PM10,
ITEM_SENSOR_CO2,PM2.5 Air Quality (CO₂-Sensor),sensor-co2-pm25,0x70,4,,,PM25,
ITEM_SENSOR_CO2,PM2.5 Air Quality 24 hour average (CO₂-Sensor),sensor-co2-pm25-24-hour-average,0x70,5,,,PM25,
ITEM_SENSOR_CO2,CO₂,sensor-co2-co2,0x70,6,,,CO2,
ITEM_SENSOR_CO2,CO₂ 24 hour average,sensor-co2-co2-24-hour-average,0x70,7,,,CO2,
ITEM_SENSOR_CO2,Battery Level,,0x70,8,1,,,
ITEM_LEAF_WETNESS_CH1,Leaf Moisture channel 1,leaf-wetness-channel-1,0x72,,,moisture,PERCENTAGE,
ITEM_LEAF_WETNESS_CH2,Leaf Moisture channel 2,leaf-wetness-channel-2,0x73,,,moisture,PERCENTAGE,
ITEM_LEAF_WETNESS_CH3,Leaf Moisture channel 3,leaf-wetness-channel-3,0x74,,,moisture,PERCENTAGE,
ITEM_LEAF_WETNESS_CH4,Leaf Moisture channel 4,leaf-wetness-channel-4,0x75,,,moisture,PERCENTAGE,
ITEM_LEAF_WETNESS_CH5,Leaf Moisture channel 5,leaf-wetness-channel-5,0x76,,,moisture,PERCENTAGE,
ITEM_LEAF_WETNESS_CH6,Leaf Moisture channel 6,leaf-wetness-channel-6,0x77,,,moisture,PERCENTAGE,
ITEM_LEAF_WETNESS_CH7,Leaf Moisture channel 7,leaf-wetness-channel-7,0x78,,,moisture,PERCENTAGE,
ITEM_LEAF_WETNESS_CH8,Leaf Moisture channel 8,leaf-wetness-channel-8,0x79,,,moisture,PERCENTAGE,
1 ManufacturerName Name Channel Code Index Skip ChannelType MeasureType_DEFAULT MeasureType_ELV
2 INTEMP Indoor Temperature temperature-indoor 0x1 system:indoor-temperature TEMPERATURE
3 OUTTEMP Outdoor Temperature temperature-outdoor 0x2 system:outdoor-temperature TEMPERATURE
4 DEWPOINT Dew point temperature-dew-point 0x3 TEMPERATURE
5 WINDCHILL Wind chill temperature-wind-chill 0x4 TEMPERATURE
6 HEATINDEX Heat index temperature-heat-index 0x5 TEMPERATURE
7 INHUMI Indoor Humidity humidity-indoor 0x6 PERCENTAGE
8 OUTHUMI Outdoor Humidity humidity-outdoor 0x7 system:atmospheric-humidity PERCENTAGE
9 ABSBARO Absolutely pressure pressure-absolute 0x8 PRESSURE
10 RELBARO Relative pressure pressure-relative 0x9 system:barometric-pressure PRESSURE
11 WINDDIRECTION Wind Direction direction-wind 0xa system:wind-direction DEGREE
12 WINDSPEED Wind Speed speed-wind 0xb system:wind-speed SPEED
13 GUSTSPEED Gust Speed speed-gust 0xc system:wind-speed SPEED
14 RAINEVENT Rain Event rain-event 0xd HEIGHT HEIGHT_BIG
15 RAINRATE Rain Rate rain-rate 0xe HEIGHT_PER_HOUR HEIGHT_PER_HOUR_BIG
16 RAINHOUR Rain hour rain-hour 0xf HEIGHT HEIGHT_BIG
17 RAINDAY Rain Day rain-day 0x10 HEIGHT HEIGHT_BIG
18 RAINWEEK Rain Week rain-week 0x11 HEIGHT HEIGHT_BIG
19 RAINMONTH Rain Month rain-month 0x12 HEIGHT_BIG
20 RAINYEAR Rain Year rain-year 0x13 HEIGHT_BIG
21 RAINTOTALS Rain Totals rain-total 0x14 HEIGHT_BIG
22 LIGHT Light illumination 0x15 LUX
23 UV UV irradiation-uv 0x16 MILLIWATT_PER_SQUARE_METRE
24 UVI UV index uv-index 0x17 uv-index BYTE
25 TIME Date and time time 0x18 DATE_TIME2
26 DAYLWINDMAX Day max wind wind-max-day 0x19 max-wind-speed SPEED
27 TEMP1 Temperature 1 temperature-channel-1 0x1a TEMPERATURE
28 TEMP2 Temperature 2 temperature-channel-2 0x1b TEMPERATURE
29 TEMP3 Temperature 3 temperature-channel-3 0x1c TEMPERATURE
30 TEMP4 Temperature 4 temperature-channel-4 0x1d TEMPERATURE
31 TEMP5 Temperature 5 temperature-channel-5 0x1e TEMPERATURE
32 TEMP6 Temperature 6 temperature-channel-6 0x1f TEMPERATURE
33 TEMP7 Temperature 7 temperature-channel-7 0x20 TEMPERATURE
34 TEMP8 Temperature 8 temperature-channel-8 0x21 TEMPERATURE
35 HUMI1 Humidity 1 humidity-channel-1 0x22 PERCENTAGE
36 HUMI2 Humidity 2 humidity-channel-2 0x23 PERCENTAGE
37 HUMI3 Humidity 3 humidity-channel-3 0x24 PERCENTAGE
38 HUMI4 Humidity 4 humidity-channel-4 0x25 PERCENTAGE
39 HUMI5 Humidity 5 humidity-channel-5 0x26 PERCENTAGE
40 HUMI6 Humidity 6 humidity-channel-6 0x27 PERCENTAGE
41 HUMI7 Humidity 7 humidity-channel-7 0x28 PERCENTAGE
42 HUMI8 Humidity 8 humidity-channel-8 0x29 PERCENTAGE
43 SOILTEMP1 Soil Temperature 1 temperature-soil-channel-1 0x2b TEMPERATURE
44 SOILTEMP2 Soil Temperature 2 temperature-soil-channel-2 0x2d TEMPERATURE
45 SOILTEMP3 Soil Temperature 3 temperature-soil-channel-3 0x2f TEMPERATURE
46 SOILTEMP4 Soil Temperature 4 temperature-soil-channel-4 0x31 TEMPERATURE
47 SOILTEMP5 Soil Temperature 5 temperature-soil-channel-5 0x33 TEMPERATURE
48 SOILTEMP6 Soil Temperature 6 temperature-soil-channel-6 0x35 TEMPERATURE
49 SOILTEMP7 Soil Temperature 7 temperature-soil-channel-7 0x37 TEMPERATURE
50 SOILTEMP8 Soil Temperature 8 temperature-soil-channel-8 0x39 TEMPERATURE
51 SOILTEMP9 Soil Temperature 9 temperature-soil-channel-9 0x3b TEMPERATURE
52 SOILTEMP10 Soil Temperature 10 temperature-soil-channel-10 0x3d TEMPERATURE
53 SOILTEMP11 Soil Temperature 11 temperature-soil-channel-11 0x3f TEMPERATURE
54 SOILTEMP12 Soil Temperature 12 temperature-soil-channel-12 0x41 TEMPERATURE
55 SOILTEMP13 Soil Temperature 13 temperature-soil-channel-13 0x43 TEMPERATURE
56 SOILTEMP14 Soil Temperature 14 temperature-soil-channel-14 0x45 TEMPERATURE
57 SOILTEMP15 Soil Temperature 15 temperature-soil-channel-15 0x47 TEMPERATURE
58 SOILTEMP16 Soil Temperature 16 temperature-soil-channel-16 0x49 TEMPERATURE
59 SOILMOISTURE1 Soil Moisture 1 moisture-soil-channel-1 0x2c moisture PERCENTAGE
60 SOILMOISTURE2 Soil Moisture 2 moisture-soil-channel-2 0x2e moisture PERCENTAGE
61 SOILMOISTURE3 Soil Moisture 3 moisture-soil-channel-3 0x30 moisture PERCENTAGE
62 SOILMOISTURE4 Soil Moisture 4 moisture-soil-channel-4 0x32 moisture PERCENTAGE
63 SOILMOISTURE5 Soil Moisture 5 moisture-soil-channel-5 0x34 moisture PERCENTAGE
64 SOILMOISTURE6 Soil Moisture 6 moisture-soil-channel-6 0x36 moisture PERCENTAGE
65 SOILMOISTURE7 Soil Moisture 7 moisture-soil-channel-7 0x38 moisture PERCENTAGE
66 SOILMOISTURE8 Soil Moisture 8 moisture-soil-channel-8 0x3a moisture PERCENTAGE
67 SOILMOISTURE9 Soil Moisture 9 moisture-soil-channel-9 0x3c moisture PERCENTAGE
68 SOILMOISTURE10 Soil Moisture 10 moisture-soil-channel-10 0x3e moisture PERCENTAGE
69 SOILMOISTURE11 Soil Moisture 11 moisture-soil-channel-11 0x40 moisture PERCENTAGE
70 SOILMOISTURE12 Soil Moisture 12 moisture-soil-channel-12 0x42 moisture PERCENTAGE
71 SOILMOISTURE13 Soil Moisture 13 moisture-soil-channel-13 0x44 moisture PERCENTAGE
72 SOILMOISTURE14 Soil Moisture 14 moisture-soil-channel-14 0x46 moisture PERCENTAGE
73 SOILMOISTURE15 Soil Moisture 15 moisture-soil-channel-15 0x48 moisture PERCENTAGE
74 SOILMOISTURE16 Soil Moisture 16 moisture-soil-channel-16 0x4a moisture PERCENTAGE
75 LOWBATT Low Battery 0x4c 1
76 PM25_24HAVG1 PM2.5 Air Quality 24 hour average channel 1 air-quality-24-hour-average-channel-1 0x4d PM25
77 PM25_24HAVG2 PM2.5 Air Quality 24 hour average channel 2 air-quality-24-hour-average-channel-2 0x4e PM25
78 PM25_24HAVG3 PM2.5 Air Quality 24 hour average channel 3 air-quality-24-hour-average-channel-3 0x4f PM25
79 PM25_24HAVG4 PM2.5 Air Quality 24 hour average channel 4 air-quality-24-hour-average-channel-4 0x50 PM25
80 PM25_CH1 PM2.5 Air Quality channel 1 air-quality-channel-1 0x2a PM25
81 PM25_CH2 PM2.5 Air Quality channel 2 air-quality-channel-2 0x51 PM25
82 PM25_CH3 PM2.5 Air Quality channel 3 air-quality-channel-3 0x52 PM25
83 PM25_CH4 PM2.5 Air Quality channel 4 air-quality-channel-4 0x53 PM25
84 LEAK_CH1 Leak channel 1 water-leak-channel-1 0x58 WATER_LEAK_DETECTION
85 LEAK_CH2 Leak channel 2 water-leak-channel-2 0x59 WATER_LEAK_DETECTION
86 LEAK_CH3 Leak channel 3 water-leak-channel-3 0x5a WATER_LEAK_DETECTION
87 LEAK_CH4 Leak channel 4 water-leak-channel-4 0x5b WATER_LEAK_DETECTION
88 LIGHTNING lightning distance 1~40KM lightning-distance 0x60 LIGHTNING_DISTANCE
89 LIGHTNING_TIME lightning happened time lightning-time 0x61 LIGHTNING_TIME
90 LIGHTNING_POWER lightning counter for the day lightning-counter 0x62 LIGHTNING_COUNTER
91 TF_USR1 Soil or Water temperature channel 1 temperature-external-channel-1 0x63 TEMPERATURE
92 TF_USR2 Soil or Water temperature channel 2 temperature-external-channel-2 0x64 TEMPERATURE
93 TF_USR3 Soil or Water temperature channel 3 temperature-external-channel-3 0x65 TEMPERATURE
94 TF_USR4 Soil or Water temperature channel 4 temperature-external-channel-4 0x66 TEMPERATURE
95 TF_USR5 Soil or Water temperature channel 5 temperature-external-channel-5 0x67 TEMPERATURE
96 TF_USR6 Soil or Water temperature channel 6 temperature-external-channel-6 0x68 TEMPERATURE
97 TF_USR7 Soil or Water temperature channel 7 temperature-external-channel-7 0x69 TEMPERATURE
98 TF_USR8 Soil or Water temperature channel 8 temperature-external-channel-8 0x6a TEMPERATURE
99 ITEM_SENSOR_CO2 Temperature (CO₂-Sensor) sensor-co2-temperature 0x70 0 TEMPERATURE
100 ITEM_SENSOR_CO2 Humidity (CO₂-Sensor) sensor-co2-humidity 0x70 1 PERCENTAGE
101 ITEM_SENSOR_CO2 PM10 Air Quality (CO₂-Sensor) sensor-co2-pm10 0x70 2 PM10
102 ITEM_SENSOR_CO2 PM10 Air Quality 24 hour average (CO₂-Sensor) sensor-co2-pm10-24-hour-average 0x70 3 PM10
103 ITEM_SENSOR_CO2 PM2.5 Air Quality (CO₂-Sensor) sensor-co2-pm25 0x70 4 PM25
104 ITEM_SENSOR_CO2 PM2.5 Air Quality 24 hour average (CO₂-Sensor) sensor-co2-pm25-24-hour-average 0x70 5 PM25
105 ITEM_SENSOR_CO2 CO₂ sensor-co2-co2 0x70 6 CO2
106 ITEM_SENSOR_CO2 CO₂ 24 hour average sensor-co2-co2-24-hour-average 0x70 7 CO2
107 ITEM_SENSOR_CO2 Battery Level 0x70 8 1
108 ITEM_LEAF_WETNESS_CH1 Leaf Moisture channel 1 leaf-wetness-channel-1 0x72 moisture PERCENTAGE
109 ITEM_LEAF_WETNESS_CH2 Leaf Moisture channel 2 leaf-wetness-channel-2 0x73 moisture PERCENTAGE
110 ITEM_LEAF_WETNESS_CH3 Leaf Moisture channel 3 leaf-wetness-channel-3 0x74 moisture PERCENTAGE
111 ITEM_LEAF_WETNESS_CH4 Leaf Moisture channel 4 leaf-wetness-channel-4 0x75 moisture PERCENTAGE
112 ITEM_LEAF_WETNESS_CH5 Leaf Moisture channel 5 leaf-wetness-channel-5 0x76 moisture PERCENTAGE
113 ITEM_LEAF_WETNESS_CH6 Leaf Moisture channel 6 leaf-wetness-channel-6 0x77 moisture PERCENTAGE
114 ITEM_LEAF_WETNESS_CH7 Leaf Moisture channel 7 leaf-wetness-channel-7 0x78 moisture PERCENTAGE
115 ITEM_LEAF_WETNESS_CH8 Leaf Moisture channel 8 leaf-wetness-channel-8 0x79 moisture PERCENTAGE

View File

@ -21,6 +21,7 @@ import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext; import org.openhab.binding.fineoffsetweatherstation.internal.domain.ConversionContext;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.Protocol;
import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
/** /**
@ -28,11 +29,9 @@ import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.Mea
*/ */
@NonNullByDefault @NonNullByDefault
class FineOffsetDataParserTest { class FineOffsetDataParserTest {
private final FineOffsetDataParser parser = new FineOffsetDataParser();
@Test @Test
void testLiveDataWH45() { void testLiveDataWH45() {
List<MeasuredValue> data = parser.getLiveData(Hex.decode( List<MeasuredValue> data = new FineOffsetDataParser(Protocol.DEFAULT).getMeasuredValues(Hex.decode(
"FFFF2700510100D306280827EF0927EF020045074F0A00150B00000C0000150000000016000117001900000E0000100000110021120000002113000005850D00007000D12E0060005A005B005502AE028F0633"), "FFFF2700510100D306280827EF0927EF020045074F0A00150B00000C0000150000000016000117001900000E0000100000110021120000002113000005850D00007000D12E0060005A005B005502AE028F0633"),
new ConversionContext(ZoneOffset.UTC)); new ConversionContext(ZoneOffset.UTC));
Assertions.assertThat(data) Assertions.assertThat(data)
@ -42,7 +41,7 @@ class FineOffsetDataParserTest {
new Tuple("temperature-outdoor", "6.9 °C"), new Tuple("humidity-outdoor", "79 %"), new Tuple("temperature-outdoor", "6.9 °C"), new Tuple("humidity-outdoor", "79 %"),
new Tuple("direction-wind", "21 °"), new Tuple("speed-wind", "0 m/s"), new Tuple("direction-wind", "21 °"), new Tuple("speed-wind", "0 m/s"),
new Tuple("speed-gust", "0 m/s"), new Tuple("illumination", "0 lx"), new Tuple("speed-gust", "0 m/s"), new Tuple("illumination", "0 lx"),
new Tuple("irradiation-uv", "1 µW/c"), new Tuple("uv-index", "0"), new Tuple("irradiation-uv", "0.1 mW/"), new Tuple("uv-index", "0"),
new Tuple("wind-max-day", "0 m/s"), new Tuple("rain-rate", "0 mm/h"), new Tuple("wind-max-day", "0 m/s"), new Tuple("rain-rate", "0 mm/h"),
new Tuple("rain-day", "0 mm"), new Tuple("rain-week", "3.3 mm"), new Tuple("rain-day", "0 mm"), new Tuple("rain-week", "3.3 mm"),
new Tuple("rain-month", "3.3 mm"), new Tuple("rain-year", "141.3 mm"), new Tuple("rain-month", "3.3 mm"), new Tuple("rain-year", "141.3 mm"),
@ -53,4 +52,32 @@ class FineOffsetDataParserTest {
new Tuple("sensor-co2-pm25-24-hour-average", "8.5 µg/m³"), new Tuple("sensor-co2-pm25-24-hour-average", "8.5 µg/m³"),
new Tuple("sensor-co2-co2", "686 ppm"), new Tuple("sensor-co2-co2-24-hour-average", "655 ppm")); new Tuple("sensor-co2-co2", "686 ppm"), new Tuple("sensor-co2-co2-24-hour-average", "655 ppm"));
} }
@Test
void testLiveDataELV() {
byte[] data = Hex.decode(
"FFFF0B00500401010B0201120300620401120501120629072108254B09254B0A01480B00040C000A0E000000001000000021110000002E120000014F130000100714000012FD15000B4BB816086917056D35");
List<MeasuredValue> measuredValues = new FineOffsetDataParser(Protocol.ELV).getMeasuredValues(data,
new ConversionContext(ZoneOffset.UTC));
Assertions.assertThat(measuredValues)
.extracting(MeasuredValue::getChannelId, measuredValue -> measuredValue.getState().toString())
.containsExactly(new Tuple("temperature-indoor", "26.7 °C"),
new Tuple("temperature-outdoor", "27.4 °C"), new Tuple("temperature-dew-point", "9.8 °C"),
new Tuple("temperature-wind-chill", "27.4 °C"), new Tuple("temperature-heat-index", "27.4 °C"),
new Tuple("humidity-indoor", "41 %"), new Tuple("humidity-outdoor", "33 %"),
new Tuple("pressure-absolute", "954.7 hPa"), new Tuple("pressure-relative", "954.7 hPa"),
new Tuple("direction-wind", "328 °"), new Tuple("speed-wind", "0.4 m/s"),
new Tuple("speed-gust", "1 m/s"), new Tuple("rain-rate", "0 mm/h"),
new Tuple("rain-day", "3.3 mm"), new Tuple("rain-week", "4.6 mm"),
new Tuple("rain-month", "33.5 mm"), new Tuple("rain-year", "410.3 mm"),
new Tuple("rain-total", "486.1 mm"), new Tuple("illumination", "74028 lx"),
new Tuple("irradiation-uv", "215.3 mW/m²"), new Tuple("uv-index", "5"));
}
@Test
void testFirmware() {
byte[] data = Hex.decode("FFFF501511456173795765617468657256312E362E3400");
String firmware = new FineOffsetDataParser(Protocol.ELV).getFirmwareVersion(data);
Assertions.assertThat(firmware).isEqualTo("EasyWeatherV1.6.4");
}
} }