2 * Copyright (c) 2010-2024 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_FREE_HEAP_SIZE;
16 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_MAX_WIND_SPEED;
17 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_MOISTURE;
18 import static org.openhab.binding.fineoffsetweatherstation.internal.FineOffsetWeatherStationBindingConstants.CHANNEL_TYPE_UV_INDEX;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
25 import java.util.Optional;
26 import java.util.stream.Collectors;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.fineoffsetweatherstation.internal.domain.response.MeasuredValue;
31 import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
32 import org.openhab.core.thing.type.ChannelTypeUID;
33 import org.openhab.core.types.State;
36 * The measurands of supported by the gateway.
38 * @author Andreas Berger - Initial contribution
41 public enum Measurand {
43 INTEMP("temperature-indoor", 0x01, "Indoor Temperature", MeasureType.TEMPERATURE,
44 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_INDOOR_TEMPERATURE),
46 OUTTEMP("temperature-outdoor", 0x02, "Outdoor Temperature", MeasureType.TEMPERATURE,
47 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_OUTDOOR_TEMPERATURE),
49 DEWPOINT("temperature-dew-point", 0x03, "Dew point", MeasureType.TEMPERATURE),
51 WINDCHILL("temperature-wind-chill", 0x04, "Wind chill", MeasureType.TEMPERATURE),
53 HEATINDEX("temperature-heat-index", 0x05, "Heat index", MeasureType.TEMPERATURE),
55 INHUMI("humidity-indoor", 0x06, "Indoor Humidity", MeasureType.PERCENTAGE),
57 OUTHUMI("humidity-outdoor", 0x07, "Outdoor Humidity", MeasureType.PERCENTAGE,
58 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_ATMOSPHERIC_HUMIDITY),
60 ABSBARO("pressure-absolute", 0x08, "Absolutely pressure", MeasureType.PRESSURE),
62 RELBARO("pressure-relative", 0x09, "Relative pressure", MeasureType.PRESSURE,
63 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_BAROMETRIC_PRESSURE),
65 WINDDIRECTION("direction-wind", 0x0A, "Wind Direction", MeasureType.DEGREE,
66 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_DIRECTION),
68 WINDSPEED("speed-wind", 0x0B, "Wind Speed", MeasureType.SPEED,
69 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_SPEED),
71 GUSTSPEED("speed-gust", 0x0C, "Gust Speed", MeasureType.SPEED,
72 DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_TYPE_UID_WIND_SPEED),
74 RAINEVENT("rain-event", 0x0D, "Rain Event", MeasureType.HEIGHT,
75 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG)),
77 RAINRATE("rain-rate", 0x0E, "Rain Rate", MeasureType.HEIGHT_PER_HOUR,
78 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_PER_HOUR_BIG)),
80 RAINHOUR("rain-hour", 0x0F, "Rain hour", MeasureType.HEIGHT,
81 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG)),
83 RAINDAY("rain-day", 0x10, "Rain Day", MeasureType.HEIGHT,
84 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG),
85 new ParserCustomization(ParserCustomizationType.RAIN_READING, MeasureType.HEIGHT_BIG)),
87 RAINWEEK("rain-week", 0x11, "Rain Week", MeasureType.HEIGHT,
88 new ParserCustomization(ParserCustomizationType.ELV, MeasureType.HEIGHT_BIG),
89 new ParserCustomization(ParserCustomizationType.RAIN_READING, MeasureType.HEIGHT_BIG)),
91 RAINMONTH("rain-month", 0x12, "Rain Month", MeasureType.HEIGHT_BIG),
93 RAINYEAR("rain-year", 0x13, "Rain Year", MeasureType.HEIGHT_BIG),
95 RAINTOTALS("rain-total", 0x14, "Rain Totals", MeasureType.HEIGHT_BIG),
97 LIGHT("illumination", 0x15, "Light", MeasureType.LUX),
99 UV("irradiation-uv", 0x16, "UV", MeasureType.MILLIWATT_PER_SQUARE_METRE),
101 UVI("uv-index", 0x17, "UV index", MeasureType.BYTE, CHANNEL_TYPE_UV_INDEX),
103 TIME("time", 0x18, "Date and time", MeasureType.DATE_TIME2),
105 DAYLWINDMAX("wind-max-day", 0X19, "Day max wind", MeasureType.SPEED, CHANNEL_TYPE_MAX_WIND_SPEED),
107 TEMPX("temperature-channel", new int[] { 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21 }, "Temperature",
108 MeasureType.TEMPERATURE),
110 HUMIX("humidity-channel", new int[] { 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29 }, "Humidity",
111 MeasureType.PERCENTAGE),
113 SOILTEMPX("temperature-soil-channel",
114 new int[] { 0x2B, 0x2D, 0x2F, 0x31, 0x33, 0x35, 0x37, 0x39, 0x3B, 0x3D, 0x3F, 0x41, 0x43, 0x45, 0x47,
116 "Soil Temperature", MeasureType.TEMPERATURE),
118 SOILMOISTUREX("moisture-soil-channel",
119 new int[] { 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48,
121 "Soil Moisture", MeasureType.PERCENTAGE, CHANNEL_TYPE_MOISTURE),
123 // will no longer be used
124 // skip battery-level, since it is read via Command.CMD_READ_SENSOR_ID_NEW
125 LOWBATT(0x4C, new Skip(1)),
127 PM25_24HAVGX("air-quality-24-hour-average-channel", new int[] { 0x4D, 0x4E, 0x4F, 0x50 },
128 "PM2.5 Air Quality 24 hour average", MeasureType.PM25),
130 PM25_CHX("air-quality-channel", new int[] { 0x2A, 0x51, 0x52, 0x53 }, "PM2.5 Air Quality", MeasureType.PM25),
132 LEAK_CHX("water-leak-channel", new int[] { 0x58, 0x59, 0x5A, 0x5B }, "Leak", MeasureType.WATER_LEAK_DETECTION),
134 // `LIGHTNING` is the name in the spec, so we keep it here as it
135 LIGHTNING("lightning-distance", 0x60, "Lightning distance 1~40KM", MeasureType.LIGHTNING_DISTANCE),
137 LIGHTNING_TIME("lightning-time", 0x61, "Lightning happened time", MeasureType.LIGHTNING_TIME),
139 // `LIGHTNING_POWER` is the name in the spec, so we keep it here as it
140 LIGHTNING_POWER("lightning-counter", 0x62, "lightning counter for the day", MeasureType.LIGHTNING_COUNTER),
142 TF_USRX(new int[] { 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A },
143 new MeasurandParser("temperature-external-channel", "Soil or Water temperature", MeasureType.TEMPERATURE),
144 // skip battery-level, since it is read via Command.CMD_READ_SENSOR_ID_NEW
147 // This is for heap : the available stack top. If it is reducing, it means the stack is using up.
148 ITEM_HEAP_FREE("free-heap-size", 0x6c, "Free Heap Size", MeasureType.MEMORY, CHANNEL_TYPE_FREE_HEAP_SIZE),
150 ITEM_SENSOR_CO2(0x70,
151 new MeasurandParser("sensor-co2-temperature", "Temperature (CO₂-Sensor)", MeasureType.TEMPERATURE),
152 new MeasurandParser("sensor-co2-humidity", "Humidity (CO₂-Sensor)", MeasureType.PERCENTAGE),
153 new MeasurandParser("sensor-co2-pm10", "PM10 Air Quality (CO₂-Sensor)", MeasureType.PM10),
154 new MeasurandParser("sensor-co2-pm10-24-hour-average", "PM10 Air Quality 24 hour average (CO₂-Sensor)",
156 new MeasurandParser("sensor-co2-pm25", "PM2.5 Air Quality (CO₂-Sensor)", MeasureType.PM25),
157 new MeasurandParser("sensor-co2-pm25-24-hour-average", "PM2.5 Air Quality 24 hour average (CO₂-Sensor)",
159 new MeasurandParser("sensor-co2-co2", "CO₂", MeasureType.CO2),
160 new MeasurandParser("sensor-co2-co2-24-hour-average", "CO₂ 24 hour average", MeasureType.CO2),
161 // skip battery-level, since it is read via Command.CMD_READ_SENSOR_ID_NEW
164 LEAF_WETNESS_CHX("leaf-wetness-channel", new int[] { 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79 },
165 "Leaf Moisture", MeasureType.PERCENTAGE, CHANNEL_TYPE_MOISTURE),
168 * 1 Traditional rain gauge
169 * 2 Piezoelectric rain gauge
171 RAIN_PRIO(0x7a, new Skip(1)),
178 RCSATION(0x7b, new Skip(1)),
180 PIEZO_RAIN_RATE("piezo-rain-rate", 0x80, "Rain Rate", MeasureType.HEIGHT_PER_HOUR),
182 PIEZO_EVENT_RAIN("piezo-rain-event", 0x81, "Rain Event", MeasureType.HEIGHT),
184 PIEZO_HOURLY_RAIN("piezo-rain-hour", 0x82, "Rain hour", MeasureType.HEIGHT),
186 PIEZO_DAILY_RAIN("piezo-rain-day", 0x83, "Rain Day", MeasureType.HEIGHT_BIG),
188 PIEZO_WEEKLY_RAIN("piezo-rain-week", 0x84, "Rain Week", MeasureType.HEIGHT_BIG),
190 PIEZO_MONTHLY_RAIN("piezo-rain-month", 0x85, "Rain Month", MeasureType.HEIGHT_BIG),
192 PIEZO_YEARLY_RAIN("piezo-rain-year", 0x86, "Rain Year", MeasureType.HEIGHT_BIG),
194 PIEZO_GAIN10(0x87, new Skip(2 * 10)),
196 RST_RAIN_TIME(0x88, new Skip(3)),
200 private static final Map<Byte, SingleChannelMeasurand> MEASURANDS = new HashMap<>();
203 for (Measurand measurand : values()) {
204 for (int i = 0; i < measurand.codes.length; i++) {
205 int code = measurand.codes[i];
206 // if we get more than one code this measurand has multiple channels
207 Integer channel = measurand.codes.length == 1 ? null : i + 1;
208 MEASURANDS.put((byte) code, new SingleChannelMeasurand(measurand, channel));
213 private final int[] codes;
214 private final Parser[] parsers;
216 Measurand(String channelId, int code, String name, MeasureType measureType, ParserCustomization... customizations) {
217 this(channelId, code, name, measureType, null, customizations);
220 Measurand(String channelId, int[] codes, String name, MeasureType measureType,
221 ParserCustomization... customizations) {
222 this(channelId, codes, name, measureType, null, customizations);
225 Measurand(String channelId, int code, String name, MeasureType measureType, @Nullable ChannelTypeUID channelTypeUID,
226 ParserCustomization... customizations) {
227 this(code, new MeasurandParser(channelId, name, measureType, channelTypeUID, customizations));
230 Measurand(String channelId, int[] codes, String name, MeasureType measureType,
231 @Nullable ChannelTypeUID channelTypeUID, ParserCustomization... customizations) {
232 this(codes, new MeasurandParser(channelId, name, measureType, channelTypeUID, customizations));
235 Measurand(int code, Parser... parsers) {
236 this(new int[] { code }, parsers);
239 Measurand(int[] codes, Parser... parsers) {
241 this.parsers = parsers;
244 public static @Nullable SingleChannelMeasurand getByCode(byte code) {
245 return MEASURANDS.get(code);
248 private int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
249 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result,
250 DebugDetails debugDetails) {
252 for (Parser parser : parsers) {
253 subOffset += parser.extractMeasuredValues(data, offset + subOffset, channel, context, customizationType,
254 result, debugDetails);
259 private interface Parser {
260 int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
261 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result,
262 DebugDetails debugDetails);
265 private static class Skip implements Parser {
266 private final int skip;
268 public Skip(int skip) {
273 public int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
274 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result,
275 DebugDetails debugDetails) {
276 debugDetails.addDebugDetails(offset, skip, "skipped");
281 public enum ParserCustomizationType {
286 private static class ParserCustomization {
288 private final ParserCustomizationType type;
289 private final MeasureType measureType;
291 public ParserCustomization(ParserCustomizationType type, MeasureType measureType) {
293 this.measureType = measureType;
296 public ParserCustomizationType getType() {
300 public MeasureType getMeasureType() {
305 public static class SingleChannelMeasurand {
306 private final Measurand measurand;
307 private final @Nullable Integer channel;
309 public SingleChannelMeasurand(Measurand measurand, @Nullable Integer channel) {
310 this.measurand = measurand;
311 this.channel = channel;
314 public int extractMeasuredValues(byte[] data, int offset, ConversionContext context,
315 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result,
316 DebugDetails debugDetails) {
317 return measurand.extractMeasuredValues(data, offset, channel, context, customizationType, result,
321 public String getDebugString() {
322 return measurand.name() + (channel == null ? "" : " channel " + channel);
326 private static class MeasurandParser implements Parser {
327 private final String name;
328 private final String channelPrefix;
329 private final MeasureType measureType;
331 private final @Nullable Map<ParserCustomizationType, ParserCustomization> customizations;
332 private final @Nullable ChannelTypeUID channelTypeUID;
334 MeasurandParser(String channelPrefix, String name, MeasureType measureType,
335 ParserCustomization... customizations) {
336 this(channelPrefix, name, measureType, null, customizations);
339 MeasurandParser(String channelPrefix, String name, MeasureType measureType,
340 @Nullable ChannelTypeUID channelTypeUID, ParserCustomization... customizations) {
341 this.channelPrefix = channelPrefix;
343 this.measureType = measureType;
344 this.channelTypeUID = channelTypeUID;
345 if (customizations.length == 0) {
346 this.customizations = null;
348 this.customizations = Collections.unmodifiableMap(
349 Arrays.stream(customizations).collect(Collectors.toMap(ParserCustomization::getType,
350 customization -> customization, (a, b) -> b, HashMap::new)));
355 public int extractMeasuredValues(byte[] data, int offset, @Nullable Integer channel, ConversionContext context,
356 @Nullable ParserCustomizationType customizationType, List<MeasuredValue> result,
357 DebugDetails debugDetails) {
358 MeasureType measureType = getMeasureType(customizationType);
359 State state = measureType.toState(data, offset, context);
361 debugDetails.addDebugDetails(offset, measureType.getByteSize(),
362 measureType.name() + ": " + state.toFullString());
363 ChannelTypeUID channelType = channelTypeUID == null ? measureType.getChannelTypeId() : channelTypeUID;
364 result.add(new MeasuredValue(measureType, channelPrefix, channel, channelType, state, name));
366 debugDetails.addDebugDetails(offset, measureType.getByteSize(), measureType.name() + ": null");
368 return measureType.getByteSize();
371 public MeasureType getMeasureType(@Nullable ParserCustomizationType customizationType) {
372 if (customizationType == null) {
375 return Optional.ofNullable(customizations).map(m -> m.get(customizationType))
376 .map(ParserCustomization::getMeasureType).orElse(measureType);