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.mqtt.ruuvigateway.internal.parser;
15 import java.nio.charset.StandardCharsets;
16 import java.time.DateTimeException;
17 import java.time.Instant;
18 import java.util.Arrays;
19 import java.util.Optional;
20 import java.util.function.Predicate;
21 import java.util.regex.Pattern;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.util.HexUtils;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
29 import com.google.gson.Gson;
30 import com.google.gson.GsonBuilder;
31 import com.google.gson.JsonSyntaxException;
33 import fi.tkgwf.ruuvi.common.bean.RuuviMeasurement;
34 import fi.tkgwf.ruuvi.common.parser.impl.AnyDataFormatParser;
37 * The {@link GatewayPayloadParser} is responsible for parsing Ruuvi Gateway MQTT JSON payloads.
39 * @author Sami Salonen - Initial contribution
42 public class GatewayPayloadParser {
44 private static final Logger logger = LoggerFactory.getLogger(GatewayPayloadParser.class);
45 private static final Gson GSON = new GsonBuilder().create();
46 private static final AnyDataFormatParser parser = new AnyDataFormatParser();
47 private static final Predicate<String> HEX_PATTERN_CHECKER = Pattern.compile("^([0-9A-Fa-f]{2})+$")
51 * JSON MQTT payload sent by Ruuvi Gateway
53 * See https://docs.ruuvi.com/gw-data-formats/mqtt-time-stamped-data-from-bluetooth-sensors
55 * @author Sami Salonen - Initial contribution
58 public static class GatewayPayload {
60 * MAC-address of Ruuvi Gateway
62 public Optional<String> gwMac = Optional.empty();
68 * Timestamp when the message from Bluetooth-sensor was relayed by Gateway
71 public Optional<Instant> gwts = Optional.empty();
74 * Timestamp (Unix-time) when the message from Bluetooth-sensor was received by Gateway
77 public Optional<Instant> ts = Optional.empty();
78 public RuuviMeasurement measurement;
80 private GatewayPayload(GatewayPayloadIntermediate intermediate) throws IllegalArgumentException {
81 String gwMac = intermediate.gw_mac;
83 logger.trace("Missing mandatory field 'gw_mac', ignoring");
85 this.gwMac = Optional.ofNullable(gwMac);
86 rssi = intermediate.rssi;
88 gwts = Optional.of(Instant.ofEpochSecond(intermediate.gwts));
89 } catch (DateTimeException e) {
90 logger.debug("Field 'gwts' is a not valid time (epoch second), ignoring: {}", intermediate.gwts);
93 ts = Optional.of(Instant.ofEpochSecond(intermediate.ts));
94 } catch (DateTimeException e) {
95 logger.debug("Field 'ts' is a not valid time (epoch second), ignoring: {}", intermediate.ts);
98 String localData = intermediate.data;
99 if (localData == null) {
100 throw new IllegalArgumentException("Missing mandatory field 'data'");
103 if (!HEX_PATTERN_CHECKER.test(localData)) {
105 "Data is not representing manufacturer specific bluetooth advertisement, it is not valid hex: {}",
107 throw new IllegalArgumentException(
108 "Data is not representing manufacturer specific bluetooth advertisement, it is not valid hex: "
111 byte[] bytes = HexUtils.hexToBytes(localData);
112 if (bytes.length < 6) {
113 // We want at least 6 bytes, ensuring bytes[5] is valid as well as Arrays.copyOfRange(bytes, 5, ...)
115 // The payload length (might depend on format version ) is validated by parser.parse call
116 throw new IllegalArgumentException("Manufacturerer data is too short");
118 if ((bytes[4] & 0xff) != 0xff) {
119 logger.debug("Data is not representing manufacturer specific bluetooth advertisement: {}",
120 HexUtils.bytesToHex(bytes));
121 throw new IllegalArgumentException(
122 "Data is not representing manufacturer specific bluetooth advertisement");
124 // Manufacturer data starts after 0xFF byte, at index 5
125 byte[] manufacturerData = Arrays.copyOfRange(bytes, 5, bytes.length);
126 RuuviMeasurement localManufacturerData = parser.parse(manufacturerData);
127 if (localManufacturerData == null) {
128 logger.trace("Manufacturer data is not valid: {}", HexUtils.bytesToHex(manufacturerData));
129 throw new IllegalArgumentException("Manufacturer data is not valid");
131 measurement = localManufacturerData;
137 * JSON MQTT payload sent by Ruuvi Gateway (intermediate representation).
139 * This intermediate representation tries to match the low level JSON, making little data validation and conversion.
141 * Fields are descibed in https://docs.ruuvi.com/gw-data-formats/mqtt-time-stamped-data-from-bluetooth-sensors
143 * Fields are marked as nullable as GSON might apply nulls at runtime.
145 * @author Sami Salonen - Initial Contribution
146 * @see GatewayPayload Equivalent of this class but with additional data validation and typing
149 private static class GatewayPayloadIntermediate {
150 public @Nullable String gw_mac;
154 public @Nullable String data;
158 * Parse MQTT JSON payload advertised by Ruuvi Gateway
160 * @param jsonPayload json payload of the Ruuvi sensor MQTT topic, as bytes
161 * @return parsed payload
162 * @throws JsonSyntaxException raised with JSON syntax exceptions and clearly invalid JSON types
163 * @throws IllegalArgumentException raised with invalid or unparseable data
165 public static GatewayPayload parse(byte[] jsonPayload) throws JsonSyntaxException, IllegalArgumentException {
166 String jsonPayloadString = new String(jsonPayload, StandardCharsets.UTF_8);
167 GatewayPayloadIntermediate payloadIntermediate = GSON.fromJson(jsonPayloadString,
168 GatewayPayloadIntermediate.class);
169 if (payloadIntermediate == null) {
170 throw new JsonSyntaxException("JSON parsing failed");
172 return new GatewayPayload(payloadIntermediate);