diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/README.md b/bundles/org.openhab.binding.fineoffsetweatherstation/README.md index 7fc57f126ff..d55b9474cff 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/README.md +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/README.md @@ -7,6 +7,7 @@ Some of these brands are e.g.: * Aercus * Ambient Weather * Ecowitt +* ELV * Frogitt * Misol * Pantech @@ -40,6 +41,7 @@ This binding works offline by [implementing the wire protocol](https://osswww.ec - WH2680 - WH2900 - WH2950 + - WS980 ELV (tested) - `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. 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 -| Name | Type | Description | Default | Required | Advanced | -|-------------------|---------|-------------------------------------------------------------------------------------|---------|----------|----------| -| 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 | -| 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 | +| Name | Type | Description | Default | Required | Advanced | +|------------------|---------|----------------------------------------------------------------------------------------------|---------|----------|----------| +| 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 | +| protocol | text | The protocol to use for communicating with the gateway, valid values are: `DEFAULT` or `ELV` | DEFAULT | no | no | +| 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 @@ -267,7 +270,13 @@ This is an example configuration for the WH2650 gateway _weatherstation.things_: ```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 WH65 "WH65" [sensor="WH65"] } diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/pom.xml b/bundles/org.openhab.binding.fineoffsetweatherstation/pom.xml index 3f766a2a351..7eefa47fc3b 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/pom.xml +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/pom.xml @@ -26,6 +26,11 @@ openHAB Add-ons :: Bundles :: Fine Offset Weather Station + + org.apache.commons + commons-csv + 1.9.0 + org.assertj assertj-core diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/FineOffsetGatewayConfiguration.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/FineOffsetGatewayConfiguration.java index 6c8e43f0264..da1613ee230 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/FineOffsetGatewayConfiguration.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/FineOffsetGatewayConfiguration.java @@ -14,6 +14,7 @@ package org.openhab.binding.fineoffsetweatherstation.internal; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.fineoffsetweatherstation.internal.domain.Protocol; /** * 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 PORT = "port"; + public static final String PROTOCOL = "protocol"; + public @Nullable String ip; public int port = 45000; public int pollingInterval = 16; public int discoverInterval = 900; + + public Protocol protocol = Protocol.DEFAULT; } diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/discovery/FineOffsetGatewayDiscoveryService.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/discovery/FineOffsetGatewayDiscoveryService.java index 10289e09549..552c2e6b35c 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/discovery/FineOffsetGatewayDiscoveryService.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/discovery/FineOffsetGatewayDiscoveryService.java @@ -21,10 +21,12 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketException; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledFuture; 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.Utils; 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.service.GatewayQueryService; +import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; import org.openhab.core.config.discovery.DiscoveryResultBuilder; @@ -60,13 +67,13 @@ import org.slf4j.LoggerFactory; @NonNullByDefault @Component(service = { DiscoveryService.class, FineOffsetGatewayDiscoveryService.class }, immediate = true) 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 final Logger logger = LoggerFactory.getLogger(FineOffsetGatewayDiscoveryService.class); 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 LocaleProvider localeProvider; private final @Nullable Bundle bundle; @@ -124,7 +131,11 @@ public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService private void discover() { 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 sensorDevices) { @@ -168,15 +179,39 @@ public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService properties.put(Thing.PROPERTY_MAC_ADDRESS, Utils.toHexString(macAddr, macAddr.length, ":")); properties.put(FineOffsetGatewayConfiguration.IP, ip); 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); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties) + .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS) .withLabel(translationProvider.getText(bundle, "thing.gateway.label", name, localeProvider.getLocale())) .build(); thingDiscovered(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 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() { DatagramSocket clientSocket = this.clientSocket; if (clientSocket != null && clientSocket.isBound()) { @@ -205,12 +240,13 @@ public class FineOffsetGatewayDiscoveryService extends AbstractDiscoveryService this.clientSocket = null; } - private void sendDiscoveryRequest(String broadcastAddress) { + private void sendBroadcastPacket(String broadcastAddress, byte[] requestMessage) { final @Nullable DatagramSocket socket = getSocket(); if (socket != null) { - byte[] requestMessage = Command.CMD_BROADCAST.getPayload(); InetSocketAddress addr = new InetSocketAddress(broadcastAddress, DISCOVERY_PORT); DatagramPacket datagramPacket = new DatagramPacket(requestMessage, requestMessage.length, addr); + logger.trace("sendBroadcastPacket: send request: {}", + Utils.toHexString(requestMessage, requestMessage.length, "")); try { socket.send(datagramPacket); } catch (IOException e) { diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Command.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Command.java index 2d47269a5a6..0d044fae920 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Command.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Command.java @@ -12,10 +12,7 @@ */ package org.openhab.binding.fineoffsetweatherstation.internal.domain; -import java.util.Arrays; - import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.fineoffsetweatherstation.internal.Utils; /** @@ -25,6 +22,12 @@ import org.openhab.binding.fineoffsetweatherstation.internal.Utils; */ @NonNullByDefault public enum Command { + + /** + * read current data,reply data size is 2bytes. + */ + CMD_WS980_LIVEDATA((byte) 0x0b, 2), + /** * send SSID and Password to WIFI module */ @@ -238,21 +241,18 @@ public enum Command { this.sizeBytes = sizeBytes; } - public byte getCode() { - return code; - } - - public int getSizeBytes() { - return sizeBytes; - } - public byte[] getPayload() { byte size = 3; // + rest of payload / not yet implemented return new byte[] { (byte) 0xff, (byte) 0xff, code, size, (byte) (code + size) }; } - public static @Nullable Command findByCode(byte code) { - return Arrays.stream(values()).filter(command -> command.getCode() == code).findFirst().orElse(null); + public byte[] getPayloadAlternative() { + 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) { diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Measurand.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Measurand.java deleted file mode 100644 index a1b2dc2b1e5..00000000000 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Measurand.java +++ /dev/null @@ -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 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 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 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 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 result) { - State state = measureType.toState(data, offset, context); - if (state != null) { - result.add(new MeasuredValue(measureType, channelId, channelTypeUID, state, name)); - } - return measureType.getByteSize(); - } - } -} diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Measurands.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Measurands.java new file mode 100644 index 00000000000..27658feb4d2 --- /dev/null +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Measurands.java @@ -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 INSTANCES = new HashMap<>(); + private final Map> 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 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 parsers = parsersPerCode.computeIfAbsent(code, aByte -> new ArrayList<>()); + // noinspection ConstantConditions + if (parsers != null) { + parsers.add(parser); + } + }); + for (List 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 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 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 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 result) { + List 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; + } +} diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/MeasureType.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/MeasureType.java index 36d16e0d685..26158d9bec5 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/MeasureType.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/MeasureType.java @@ -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.METRE; 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.METRE_PER_SECOND; 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_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.), 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( ZonedDateTime.ofInstant(Instant.ofEpochSecond(toUInt32(data, offset)), context.getZoneId()))), - MICROWATT_PER_SQUARE_CENTIMETRE(Units.MICROWATT_PER_SQUARE_CENTIMETRE, 2, CHANNEL_TYPE_UV_RADIATION, - Utils::toUInt16), + MILLIWATT_PER_SQUARE_METRE(MILLI(Units.WATT).divide(SQUARE_METRE), 2, CHANNEL_TYPE_UV_RADIATION, + (data, offset) -> Utils.toUInt16(data, offset) / 10.), BYTE(1, null, (data, offset, context) -> new DecimalType(toUInt8(data[offset]))), diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Protocol.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Protocol.java new file mode 100644 index 00000000000..0ca9da2b63a --- /dev/null +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/domain/Protocol.java @@ -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); + } +} diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetGatewayHandler.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetGatewayHandler.java index 1d37544f0bc..aa91798d413 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetGatewayHandler.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetGatewayHandler.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.function.Function; 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.SensorDevice; 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.TimeZoneProvider; 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.types.Command; import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; import org.osgi.framework.Bundle; import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; @@ -76,7 +78,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { private final Bundle bundle; private final ConversionContext conversionContext; - private @Nullable FineOffsetGatewayQueryService gatewayQueryService; + private @Nullable GatewayQueryService gatewayQueryService; private final FineOffsetGatewayDiscoveryService gatewayDiscoveryService; private final ChannelTypeRegistry channelTypeRegistry; @@ -94,7 +96,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { ChannelTypeRegistry channelTypeRegistry, TranslationProvider translationProvider, LocaleProvider localeProvider, TimeZoneProvider timeZoneProvider) { super(bridge); - bridgeUID = bridge.getUID(); + this.bridgeUID = bridge.getUID(); this.gatewayDiscoveryService = gatewayDiscoveryService; this.channelTypeRegistry = channelTypeRegistry; this.translationProvider = translationProvider; @@ -110,7 +112,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { @Override public void initialize() { FineOffsetGatewayConfiguration config = getConfigAs(FineOffsetGatewayConfiguration.class); - gatewayQueryService = new FineOffsetGatewayQueryService(config, this::updateStatus, conversionContext); + gatewayQueryService = config.protocol.getGatewayQueryService(config, this::updateStatus, conversionContext); updateStatus(ThingStatus.UNKNOWN); fetchAndUpdateSensors(); @@ -122,7 +124,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { private void fetchAndUpdateSensors() { @Nullable - Map deviceMap = query(FineOffsetGatewayQueryService::getRegisteredSensors); + Map deviceMap = query(GatewayQueryService::getRegisteredSensors); sensorDeviceMap = deviceMap; updateSensors(); if (deviceMap != null) { @@ -154,8 +156,9 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { if (disposed) { return; } - List data = query(FineOffsetGatewayQueryService::getLiveData); + List data = query(GatewayQueryService::getMeasuredValues); if (data == null) { + getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.UNDEF)); return; } @@ -174,7 +177,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { } } 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() { @Nullable - String firmware = query(FineOffsetGatewayQueryService::getFirmwareVersion); + String firmware = query(GatewayQueryService::getFirmwareVersion); Map properties = new HashMap<>(thing.getProperties()); if (firmware != null) { - var fwString = firmware.split("_V"); + var fwString = firmware.split("_?V"); if (fwString.length > 1) { properties.put(Thing.PROPERTY_MODEL_ID, fwString[0]); properties.put(Thing.PROPERTY_FIRMWARE_VERSION, fwString[1]); } } - SystemInfo systemInfo = query(FineOffsetGatewayQueryService::fetchSystemInfo); + SystemInfo systemInfo = query(GatewayQueryService::fetchSystemInfo); if (systemInfo != null && systemInfo.getFrequency() != null) { properties.put(PROPERTY_FREQUENCY, systemInfo.getFrequency() + " MHz"); } if (!thing.getProperties().equals(properties)) { - BridgeBuilder bridge = editThing(); - bridge.withProperties(properties); - updateThing(bridge.build()); + updateBridgeThing(bridgeBuilder -> bridgeBuilder.withProperties(properties)); } } + private void updateBridgeThing(Consumer customizer) { + BridgeBuilder bridge = editThing(); + customizer.accept(bridge); + updateThing(bridge.build()); + } + private void startDiscoverJob() { ScheduledFuture job = discoverJob; if (job == null || job.isCancelled()) { @@ -262,9 +269,9 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { this.pollingJob = null; } - private @Nullable T query(Function delegate) { + private @Nullable T query(Function delegate) { @Nullable - FineOffsetGatewayQueryService queryService = this.gatewayQueryService; + GatewayQueryService queryService = this.gatewayQueryService; if (queryService == null) { return null; } @@ -275,7 +282,7 @@ public class FineOffsetGatewayHandler extends BaseBridgeHandler { public void dispose() { disposed = true; @Nullable - FineOffsetGatewayQueryService queryService = this.gatewayQueryService; + GatewayQueryService queryService = this.gatewayQueryService; if (queryService != null) { try { queryService.close(); diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetSensorHandler.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetSensorHandler.java index b81894314be..df0433361f4 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetSensorHandler.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/handler/FineOffsetSensorHandler.java @@ -27,6 +27,7 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; 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. @@ -61,7 +62,10 @@ public class FineOffsetSensorHandler extends BaseThingHandler { return; } 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; } if (sensorDevice.getSignal() == 0) { diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/ELVGatewayQueryService.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/ELVGatewayQueryService.java new file mode 100644 index 00000000000..0661c0e2049 --- /dev/null +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/ELVGatewayQueryService.java @@ -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 getRegisteredSensors() { + // not supported by ELV device + return Collections.emptyMap(); + } + + @Override + public @Nullable SystemInfo fetchSystemInfo() { + // not supported by ELV device + return null; + } + + @Override + public List 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); + } +} diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParser.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParser.java index 5bcfcd8b7ae..5bd28d0c44b 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParser.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParser.java @@ -27,7 +27,8 @@ 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.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.response.BatteryStatus; import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue; @@ -44,6 +45,11 @@ import org.slf4j.LoggerFactory; @NonNullByDefault public class FineOffsetDataParser { 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) { if (data.length > 0) { @@ -145,7 +151,7 @@ public class FineOffsetDataParser { return new SystemInfo(frequency, date, dst, useWh24); } - List getLiveData(byte[] data, ConversionContext context) { + List getMeasuredValues(byte[] data, ConversionContext context) { /* * Pos| Length | Description * ------------------------------------------------- @@ -165,16 +171,20 @@ public class FineOffsetDataParser { * | 1 | checksum */ 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); List result = new ArrayList<>(); + Measurands measurands = Measurands.getInstance(protocol); while (idx < size) { byte code = data[idx++]; - Measurand measurand = Measurand.getByCode(code); - if (measurand == null) { - logger.warn("failed to get measurand 0x{}", Integer.toHexString(code)); + try { + idx += measurands.extractMeasuredValues(code, data, idx, context, result); + } catch (IllegalArgumentException e) { + logger.warn("", e); return result; } - idx += measurand.extractMeasuredValues(data, idx, context, result); } return result; } diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetGatewayQueryService.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetGatewayQueryService.java index 32950d16b45..7520f954087 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetGatewayQueryService.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetGatewayQueryService.java @@ -12,10 +12,6 @@ */ 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.List; import java.util.Map; @@ -23,16 +19,14 @@ 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.Utils; 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; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,24 +36,21 @@ import org.slf4j.LoggerFactory; * @author Andreas Berger - Initial contribution */ @NonNullByDefault -public class FineOffsetGatewayQueryService implements AutoCloseable { +public class FineOffsetGatewayQueryService extends GatewayQueryService { 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 ConversionContext conversionContext; - public FineOffsetGatewayQueryService(FineOffsetGatewayConfiguration config, ThingStatusListener thingStatusListener, - ConversionContext conversionContext) { - this.config = config; - this.thingStatusListener = thingStatusListener; - this.fineOffsetDataParser = new FineOffsetDataParser(); + public FineOffsetGatewayQueryService(FineOffsetGatewayConfiguration config, + @Nullable ThingStatusListener thingStatusListener, ConversionContext conversionContext) { + super(config, thingStatusListener); + this.fineOffsetDataParser = new FineOffsetDataParser(Protocol.DEFAULT); this.conversionContext = conversionContext; } + @Override public @Nullable String getFirmwareVersion() { var data = executeCommand(Command.CMD_READ_FIRMWARE_VERSION); if (null != data) { @@ -68,6 +59,7 @@ public class FineOffsetGatewayQueryService implements AutoCloseable { return null; } + @Override public Map getRegisteredSensors() { var data = executeCommand(Command.CMD_READ_SENSOR_ID_NEW); if (null == data) { @@ -83,6 +75,7 @@ public class FineOffsetGatewayQueryService implements AutoCloseable { }); } + @Override public @Nullable SystemInfo fetchSystemInfo() { var data = executeCommand(Command.CMD_READ_SSSS); if (data == null) { @@ -92,80 +85,16 @@ public class FineOffsetGatewayQueryService implements AutoCloseable { return fineOffsetDataParser.fetchSystemInfo(data); } - public List getLiveData() { + @Override + public List getMeasuredValues() { byte[] data = executeCommand(Command.CMD_GW1000_LIVEDATA); if (data == null) { return Collections.emptyList(); } - return fineOffsetDataParser.getLiveData(data, conversionContext); + return fineOffsetDataParser.getMeasuredValues(data, conversionContext); } - private synchronized byte @Nullable [] executeCommand(Command command) { - byte[] buffer = new byte[2028]; - 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(); - } + protected byte @Nullable [] executeCommand(Command command) { + return executeCommand(command.name(), command.getPayload(), command::isResponseValid); } } diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/GatewayQueryService.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/GatewayQueryService.java new file mode 100644 index 00000000000..4f28ac8f6ce --- /dev/null +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/java/org/openhab/binding/fineoffsetweatherstation/internal/service/GatewayQueryService.java @@ -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 getRegisteredSensors(); + + @Nullable + public abstract SystemInfo fetchSystemInfo(); + + public abstract List getMeasuredValues(); + + public GatewayQueryService(FineOffsetGatewayConfiguration config, + @Nullable ThingStatusListener thingStatusListener) { + this.config = config; + this.thingStatusListener = thingStatusListener; + } + + protected byte @Nullable [] executeCommand(String command, byte[] request, + Function 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(); + } + } +} diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/config/config-descriptions.xml b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/config/config-descriptions.xml index 72f0ba4c68e..ff86e04b445 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/config/config-descriptions.xml +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/config/config-descriptions.xml @@ -16,6 +16,11 @@ The network port of the gateway 45000 + + + The protocol to use for communicating with the gateway, valid values are: `DEFAULT` or `ELV` + DEFAULT + Polling interval for refreshing the data in seconds diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/i18n/fineoffsetweatherstation.properties b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/i18n/fineoffsetweatherstation.properties index 46b30c51816..1eda2bb2ae7 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/i18n/fineoffsetweatherstation.properties +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/i18n/fineoffsetweatherstation.properties @@ -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.port.label = Port 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 # channel types diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/gateway.xml b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/gateway.xml index b7a0753393f..062a62401a0 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/gateway.xml +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/gateway.xml @@ -8,6 +8,7 @@ A WiFi connected gateway device (WN1900, GW1000, GW1100, WH2680, WH2650) to bridge Sensors NetworkAppliance + macAddress diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/sensor.xml b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/sensor.xml index 8e3668dbc4a..3ea78110086 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/sensor.xml +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/OH-INF/thing/sensor.xml @@ -19,6 +19,7 @@ + sensor diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/measurands.csv b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/measurands.csv new file mode 100644 index 00000000000..485826300df --- /dev/null +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/main/resources/measurands.csv @@ -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, diff --git a/bundles/org.openhab.binding.fineoffsetweatherstation/src/test/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParserTest.java b/bundles/org.openhab.binding.fineoffsetweatherstation/src/test/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParserTest.java index f08356b280f..52c765b7f61 100644 --- a/bundles/org.openhab.binding.fineoffsetweatherstation/src/test/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParserTest.java +++ b/bundles/org.openhab.binding.fineoffsetweatherstation/src/test/java/org/openhab/binding/fineoffsetweatherstation/internal/service/FineOffsetDataParserTest.java @@ -21,6 +21,7 @@ import org.bouncycastle.util.encoders.Hex; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; 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; /** @@ -28,11 +29,9 @@ import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.Mea */ @NonNullByDefault class FineOffsetDataParserTest { - private final FineOffsetDataParser parser = new FineOffsetDataParser(); - @Test void testLiveDataWH45() { - List data = parser.getLiveData(Hex.decode( + List data = new FineOffsetDataParser(Protocol.DEFAULT).getMeasuredValues(Hex.decode( "FFFF2700510100D306280827EF0927EF020045074F0A00150B00000C0000150000000016000117001900000E0000100000110021120000002113000005850D00007000D12E0060005A005B005502AE028F0633"), new ConversionContext(ZoneOffset.UTC)); Assertions.assertThat(data) @@ -42,7 +41,7 @@ class FineOffsetDataParserTest { 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("speed-gust", "0 m/s"), new Tuple("illumination", "0 lx"), - new Tuple("irradiation-uv", "1 µW/cm²"), new Tuple("uv-index", "0"), + new Tuple("irradiation-uv", "0.1 mW/m²"), new Tuple("uv-index", "0"), 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-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-co2", "686 ppm"), new Tuple("sensor-co2-co2-24-hour-average", "655 ppm")); } + + @Test + void testLiveDataELV() { + byte[] data = Hex.decode( + "FFFF0B00500401010B0201120300620401120501120629072108254B09254B0A01480B00040C000A0E000000001000000021110000002E120000014F130000100714000012FD15000B4BB816086917056D35"); + List 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"); + } }