2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.fineoffsetweatherstation.internal.domain;
15 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_MAX_WIND_SPEED;
16 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_MOISTURE;
17 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_UV_INDEX;
19 import java.util.Arrays;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
24 import java.util.Optional;
25 import java.util.stream.Collectors;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
30 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
31 import org.openhab.core.thing.type.ChannelTypeUID;
32 import org.openhab.core.types.State;
35 * The measurands of supported by the gateway.
37 * @author Andreas Berger - Initial contribution
40 public enum Measurand {
42 INTEMP("temperature-indoor", 0x01, "Indoor Temperature", MeasureType.TEMPERATURE,
43 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_INDOOR_TEMPERATURE),
45 OUTTEMP("temperature-outdoor", 0x02, "Outdoor Temperature", MeasureType.TEMPERATURE,
46 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_OUTDOOR_TEMPERATURE),
48 DEWPOINT("temperature-dew-point", 0x03, "Dew point", MeasureType.TEMPERATURE),
50 WINDCHILL("temperature-wind-chill", 0x04, "Wind chill", MeasureType.TEMPERATURE),
52 HEATINDEX("temperature-heat-index", 0x05, "Heat index", MeasureType.TEMPERATURE),
54 INHUMI("humidity-indoor", 0x06, "Indoor Humidity", MeasureType.PERCENTAGE),
56 OUTHUMI("humidity-outdoor", 0x07, "Outdoor Humidity", MeasureType.PERCENTAGE,
57 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_ATMOSPHERIC_HUMIDITY),
59 ABSBARO("pressure-absolute", 0x08, "Absolutely pressure", MeasureType.PRESSURE),
61 RELBARO("pressure-relative", 0x09, "Relative pressure", MeasureType.PRESSURE,
62 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_BAROMETRIC_PRESSURE),
64 WINDDIRECTION("direction-wind", 0x0A, "Wind Direction", MeasureType.DEGREE,
65 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_DIRECTION),
67 WINDSPEED("speed-wind", 0x0B, "Wind Speed", MeasureType.SPEED,
68 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_SPEED),
70 GUSTSPEED("speed-gust", 0x0C, "Gust Speed", MeasureType.SPEED,
71 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_SPEED),
73 RAINEVENT("rain-event", 0x0D, "Rain Event", MeasureType.HEIGHT,
74 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG)),
76 RAINRATE("rain-rate", 0x0E, "Rain Rate", MeasureType.HEIGHT_PER_HOUR,
77 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_PER_HOUR_BIG)),
79 RAINHOUR("rain-hour", 0x0F, "Rain hour", MeasureType.HEIGHT,
80 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG)),
82 RAINDAY("rain-day", 0x10, "Rain Day", MeasureType.HEIGHT,
83 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG),
84 new ParserCustomization(ParserCustomizationType.RAIN_READING, MeasureType.HEIGHT_BIG)),
86 RAINWEEK("rain-week", 0x11, "Rain Week", MeasureType.HEIGHT,
87 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG),
88 new ParserCustomization(ParserCustomizationType.RAIN_READING, MeasureType.HEIGHT_BIG)),
90 RAINMONTH("rain-month", 0x12, "Rain Month", MeasureType.HEIGHT_BIG),
92 RAINYEAR("rain-year", 0x13, "Rain Year", MeasureType.HEIGHT_BIG),
94 RAINTOTALS("rain-total", 0x14, "Rain Totals", MeasureType.HEIGHT_BIG),
96 LIGHT("illumination", 0x15, "Light", MeasureType.LUX),
98 UV("irradiation-uv", 0x16, "UV", MeasureType.MILLIWATT_PER_SQUARE_METRE),
100 UVI("uv-index", 0x17, "UV index", MeasureType.BYTE, CHANNEL_TYPE_UV_INDEX),
102 TIME("time", 0x18, "Date and time", MeasureType.DATE_TIME2),
104 DAYLWINDMAX("wind-max-day", 0X19, "Day max wind", MeasureType.SPEED, CHANNEL_TYPE_MAX_WIND_SPEED),
106 TEMPX("temperature-channel", new int[] { 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21 }, "Temperature",
107 MeasureType.TEMPERATURE),
109 HUMIX("humidity-channel", new int[] { 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29 }, "Humidity",
110 MeasureType.PERCENTAGE),
112 SOILTEMPX("temperature-soil-channel",
113 new int[] { 0x2B, 0x2D, 0x2F, 0x31, 0x33, 0x35, 0x37, 0x39, 0x3B, 0x3D, 0x3F, 0x41, 0x43, 0x45, 0x47,
115 "Soil Temperature", MeasureType.TEMPERATURE),
117 SOILMOISTUREX("moisture-soil-channel",
118 new int[] { 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48,
120 "Soil Moisture", MeasureType.PERCENTAGE, CHANNEL_TYPE_MOISTURE),
122 // will no longer be used
123 // skip battery-level, since it is read via Command.CMD_READ_SENSOR_ID_NEW
124 LOWBATT(0x4C, new Skip(1)),
126 PM25_24HAVGX("air-quality-24-hour-average-channel", new int[] { 0x4D, 0x4E, 0x4F, 0x50 },
127 "PM2.5 Air Quality 24 hour average", MeasureType.PM25),
129 PM25_CHX("air-quality-channel", new int[] { 0x2A, 0x51, 0x52, 0x53 }, "PM2.5 Air Quality", MeasureType.PM25),
131 LEAK_CHX("water-leak-channel", new int[] { 0x58, 0x59, 0x5A, 0x5B }, "Leak", MeasureType.WATER_LEAK_DETECTION),
133 // `LIGHTNING` is the name in the spec, so we keep it here as it
134 LIGHTNING("lightning-distance", 0x60, "lightning distance 1~40KM", MeasureType.LIGHTNING_DISTANCE),
136 LIGHTNING_TIME("lightning-time", 0x61, "lightning happened time", MeasureType.LIGHTNING_TIME),
138 // `LIGHTNING_POWER` is the name in the spec, so we keep it here as it
139 LIGHTNING_POWER("lightning-counter", 0x62, "lightning counter for the day", MeasureType.LIGHTNING_COUNTER),
141 TF_USRX("temperature-external-channel", new int[] { 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A },
142 "Soil or Water temperature", MeasureType.TEMPERATURE),
144 ITEM_SENSOR_CO2(0x70,
145 new MeasurandParser("sensor-co2-temperature", "Temperature (CO₂-Sensor)", MeasureType.TEMPERATURE),
146 new MeasurandParser("sensor-co2-humidity", "Humidity (CO₂-Sensor)", MeasureType.PERCENTAGE),
147 new MeasurandParser("sensor-co2-pm10", "PM10 Air Quality (CO₂-Sensor)", MeasureType.PM10),
148 new MeasurandParser("sensor-co2-pm10-24-hour-average", "PM10 Air Quality 24 hour average (CO₂-Sensor)",
150 new MeasurandParser("sensor-co2-pm25", "PM2.5 Air Quality (CO₂-Sensor)", MeasureType.PM25),
151 new MeasurandParser("sensor-co2-pm25-24-hour-average", "PM2.5 Air Quality 24 hour average (CO₂-Sensor)",
153 new MeasurandParser("sensor-co2-co2", "CO₂", MeasureType.CO2),
154 new MeasurandParser("sensor-co2-co2-24-hour-average", "CO₂ 24 hour average", MeasureType.CO2),
155 // skip battery-level, since it is read via Command.CMD_READ_SENSOR_ID_NEW
158 LEAF_WETNESS_CHX("leaf-wetness-channel", new int[] { 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79 },
159 "Leaf Moisture", MeasureType.PERCENTAGE, CHANNEL_TYPE_MOISTURE),
162 * 1 Traditional rain gauge
163 * 2 Piezoelectric rain gauge
165 RAIN_PRIO(0x7a, new Skip(1)),
172 RCSATION(0x7b, new Skip(1)),
174 PIEZO_RAIN_RATE("piezo-rain-rate", 0x80, "Rain Rate", MeasureType.HEIGHT_PER_HOUR),
176 PIEZO_EVENT_RAIN("piezo-rain-event", 0x81, "Rain Event", MeasureType.HEIGHT),
178 PIEZO_HOURLY_RAIN("piezo-rain-hour", 0x82, "Rain hour", MeasureType.HEIGHT),
180 PIEZO_DAILY_RAIN("piezo-rain-day", 0x83, "Rain Day", MeasureType.HEIGHT_BIG),
182 PIEZO_WEEKLY_RAIN("piezo-rain-week", 0x84, "Rain Week", MeasureType.HEIGHT_BIG),
184 PIEZO_MONTHLY_RAIN("piezo-rain-month", 0x85, "Rain Month", MeasureType.HEIGHT_BIG),
186 PIEZO_YEARLY_RAIN("piezo-rain-year", 0x86, "Rain Year", MeasureType.HEIGHT_BIG),
188 PIEZO_GAIN10(0x87, new Skip(2 * 10)),
190 RST_RAIN_TIME(0x88, new Skip(3)),
194 private static final Map<Byte, SingleChannelMeasurand> MEASURANDS = new HashMap<>();
197 for (Measurand measurand : values()) {
198 for (int i = 0; i < measurand.codes.length; i++) {
199 int code = measurand.codes[i];
200 // if we get more than one code this measurand has multiple channels
201 Integer channel = measurand.codes.length == 1 ? null : i + 1;
202 MEASURANDS.put((byte) code, new SingleChannelMeasurand(measurand, channel));
207 private final int[] codes;
208 private final Parser[] parsers;
210 Measurand(String channelId, int code, String name, MeasureType measureType, ParserCustomization... customizations) {
211 this(channelId, code, name, measureType, null, customizations);
214 Measurand(String channelId, int[] codes, String name, MeasureType measureType,
215 ParserCustomization... customizations) {
216 this(channelId, codes, name, measureType, null, customizations);
219 Measurand(String channelId, int code, String name, MeasureType measureType, @Nullable ChannelTypeUID channelTypeUID,
220 ParserCustomization... customizations) {
221 this(code, new MeasurandParser(channelId, name, measureType, channelTypeUID, customizations));
224 Measurand(String channelId, int[] codes, String name, MeasureType measureType,
225 @Nullable ChannelTypeUID channelTypeUID, ParserCustomization... customizations) {
226 this(codes, new MeasurandParser(channelId, name, measureType, channelTypeUID, customizations));
229 Measurand(int code, Parser... parsers) {
230 this(new int[] { code }, parsers);
233 Measurand(int[] codes, Parser... parsers) {
235 this.parsers = parsers;
238 public static @Nullable SingleChannelMeasurand getByCode(byte code) {
239 return MEASURANDS.get(code);
242 private int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
243 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result) {
245 for (Parser parser : parsers) {
246 subOffset += parser.extractMeasuredValues(data, offset + subOffset, channel, context, customizationType,
252 private interface Parser {
253 int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
254 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result);
257 private static class Skip implements Parser {
258 private final int skip;
260 public Skip(int skip) {
265 public int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
266 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result) {
271 public enum ParserCustomizationType {
276 private static class ParserCustomization {
278 private final ParserCustomizationType type;
279 private final MeasureType measureType;
281 public ParserCustomization(ParserCustomizationType type, MeasureType measureType) {
283 this.measureType = measureType;
286 public ParserCustomizationType getType() {
290 public MeasureType getMeasureType() {
295 public static class SingleChannelMeasurand {
296 private final Measurand measurand;
297 private final @Nullable Integer channel;
299 public SingleChannelMeasurand(Measurand measurand, @Nullable Integer channel) {
300 this.measurand = measurand;
301 this.channel = channel;
304 public int extractMeasuredValues(byte[] data, int offset, ConversionContext context,
305 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result) {
306 return measurand.extractMeasuredValues(data, offset, channel, context, customizationType, result);
310 private static class MeasurandParser implements Parser {
311 private final String name;
312 private final String channelPrefix;
313 private final MeasureType measureType;
315 private final @Nullable Map<ParserCustomizationType, ParserCustomization> customizations;
316 private final @Nullable ChannelTypeUID channelTypeUID;
318 MeasurandParser(String channelPrefix, String name, MeasureType measureType,
319 ParserCustomization... customizations) {
320 this(channelPrefix, name, measureType, null, customizations);
323 MeasurandParser(String channelPrefix, String name, MeasureType measureType,
324 @Nullable ChannelTypeUID channelTypeUID, ParserCustomization... customizations) {
325 this.channelPrefix = channelPrefix;
327 this.measureType = measureType;
328 this.channelTypeUID = channelTypeUID;
329 if (customizations.length == 0) {
330 this.customizations = null;
332 this.customizations = Collections.unmodifiableMap(
333 Arrays.stream(customizations).collect(Collectors.toMap(ParserCustomization::getType,
334 customization -> customization, (a, b) -> b, HashMap::new)));
339 public int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
340 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result) {
341 MeasureType measureType = getMeasureType(customizationType);
342 State state = measureType.toState(data, offset, context);
344 ChannelTypeUID channelType = channelTypeUID == null ? measureType.getChannelTypeId() : channelTypeUID;
345 result.add(new MeasuredValue(measureType, channelPrefix, channel, channelType, state, name));
347 return measureType.getByteSize();
350 public MeasureType getMeasureType(@Nullable ParserCustomizationType customizationType) {
351 if (customizationType == null) {
354 return Optional.ofNullable(customizations).map(m -> m.get(customizationType))
355 .map(ParserCustomization::getMeasureType).orElse(measureType);