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.bluetooth.ruuvitag.internal;
15 import static org.openhab.binding.bluetooth.ruuvitag.internal.RuuviTagBindingConstants.*;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.atomic.AtomicBoolean;
21 import javax.measure.Quantity;
22 import javax.measure.Unit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
27 import org.openhab.binding.bluetooth.notification.BluetoothScanNotification;
28 import org.openhab.core.library.types.DecimalType;
29 import org.openhab.core.library.types.QuantityType;
30 import org.openhab.core.library.unit.SIUnits;
31 import org.openhab.core.library.unit.Units;
32 import org.openhab.core.thing.Channel;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.types.UnDefType;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
41 import fi.tkgwf.ruuvi.common.bean.RuuviMeasurement;
42 import fi.tkgwf.ruuvi.common.parser.impl.AnyDataFormatParser;
45 * The {@link RuuviTagHandler} is responsible for handling commands, which are
46 * sent to one of the channels.
48 * @author Sami Salonen - Initial contribution
51 public class RuuviTagHandler extends BeaconBluetoothHandler {
53 // Ruuvitag sends an update every 10 seconds. So we keep a heartbeat to give it some slack
54 private static final int HEARTBEAT_TIMEOUT_MINUTES = 1;
55 private final Logger logger = LoggerFactory.getLogger(RuuviTagHandler.class);
56 private final AnyDataFormatParser parser = new AnyDataFormatParser();
57 private final AtomicBoolean receivedStatus = new AtomicBoolean();
59 private @NonNullByDefault({}) ScheduledFuture<?> heartbeatFuture;
61 public RuuviTagHandler(Thing thing) {
66 public void initialize() {
68 if (getThing().getStatus() != ThingStatus.OFFLINE) {
69 heartbeatFuture = scheduler.scheduleWithFixedDelay(this::heartbeat, 0, HEARTBEAT_TIMEOUT_MINUTES,
74 private void heartbeat() {
75 synchronized (receivedStatus) {
76 if (!receivedStatus.getAndSet(false) && getThing().getStatus() == ThingStatus.ONLINE) {
77 getThing().getChannels().stream().map(Channel::getUID).filter(this::isLinked)
78 .forEach(c -> updateState(c, UnDefType.UNDEF));
79 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
80 "No data received for some time");
86 public void dispose() {
90 if (heartbeatFuture != null) {
91 heartbeatFuture.cancel(true);
92 heartbeatFuture = null;
98 public void onScanRecordReceived(BluetoothScanNotification scanNotification) {
99 synchronized (receivedStatus) {
100 receivedStatus.set(true);
101 super.onScanRecordReceived(scanNotification);
102 final byte[] manufacturerData = scanNotification.getManufacturerData();
103 if (manufacturerData != null && manufacturerData.length > 0) {
104 final RuuviMeasurement ruuvitagData = parser.parse(manufacturerData);
105 logger.trace("Ruuvi received new scan notification for {}: {}", scanNotification.getAddress(),
107 if (ruuvitagData != null) {
108 boolean atLeastOneRuuviFieldPresent = false;
109 for (Channel channel : getThing().getChannels()) {
110 ChannelUID channelUID = channel.getUID();
111 switch (channelUID.getId()) {
112 case CHANNEL_ID_ACCELERATIONX:
113 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
114 ruuvitagData.getAccelerationX(), Units.STANDARD_GRAVITY);
116 case CHANNEL_ID_ACCELERATIONY:
117 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
118 ruuvitagData.getAccelerationY(), Units.STANDARD_GRAVITY);
120 case CHANNEL_ID_ACCELERATIONZ:
121 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
122 ruuvitagData.getAccelerationZ(), Units.STANDARD_GRAVITY);
124 case CHANNEL_ID_BATTERY:
125 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
126 ruuvitagData.getBatteryVoltage(), Units.VOLT);
128 case CHANNEL_ID_DATA_FORMAT:
129 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
130 ruuvitagData.getDataFormat());
132 case CHANNEL_ID_HUMIDITY:
133 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
134 ruuvitagData.getHumidity(), Units.PERCENT);
136 case CHANNEL_ID_MEASUREMENT_SEQUENCE_NUMBER:
137 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
138 ruuvitagData.getMeasurementSequenceNumber(), Units.ONE);
140 case CHANNEL_ID_MOVEMENT_COUNTER:
141 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
142 ruuvitagData.getMovementCounter(), Units.ONE);
144 case CHANNEL_ID_PRESSURE:
145 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
146 ruuvitagData.getPressure(), SIUnits.PASCAL);
148 case CHANNEL_ID_TEMPERATURE:
149 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
150 ruuvitagData.getTemperature(), SIUnits.CELSIUS);
152 case CHANNEL_ID_TX_POWER:
153 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
154 ruuvitagData.getTxPower(), Units.DECIBEL_MILLIWATTS);
158 if (atLeastOneRuuviFieldPresent) {
159 // In practice, updated to ONLINE by super.onScanRecordReceived already, based on RSSI value
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162 "Received Ruuvi Tag data but no fields could be parsed");
165 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
166 "Received bluetooth data which could not be parsed to any known Ruuvi Tag data formats");
169 // Received Bluetooth scan with no manufacturer data
170 // This happens -- we ignore this silently.
176 * Update QuantityType channel state
178 * Update is not done when value is null.
180 * @param channelUID channel UID
181 * @param value value to update
182 * @param unit unit associated with the value
183 * @return whether the value was present
185 private <T extends Quantity<T>> boolean updateStateIfLinked(ChannelUID channelUID, @Nullable Number value,
190 if (isLinked(channelUID)) {
191 updateState(channelUID, new QuantityType<>(value, unit));
197 * Update DecimalType channel state
199 * Update is not done when value is null.
201 * @param channelUID channel UID
202 * @param value value to update
203 * @return whether the value was present
205 private <T extends Quantity<T>> boolean updateStateIfLinked(ChannelUID channelUID, @Nullable Integer value) {
209 if (isLinked(channelUID)) {
210 updateState(channelUID, new DecimalType(value));