]> git.basschouten.com Git - openhab-addons.git/blob
70aaad59ed0884554fdb210131900f7cef6534f5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.bluetooth.ruuvitag.internal;
14
15 import static org.openhab.binding.bluetooth.ruuvitag.internal.RuuviTagBindingConstants.*;
16
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.atomic.AtomicBoolean;
20
21 import javax.measure.Quantity;
22 import javax.measure.Unit;
23
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;
40
41 import fi.tkgwf.ruuvi.common.bean.RuuviMeasurement;
42 import fi.tkgwf.ruuvi.common.parser.impl.AnyDataFormatParser;
43
44 /**
45  * The {@link RuuviTagHandler} is responsible for handling commands, which are
46  * sent to one of the channels.
47  *
48  * @author Sami Salonen - Initial contribution
49  */
50 @NonNullByDefault
51 public class RuuviTagHandler extends BeaconBluetoothHandler {
52
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();
58
59     private @NonNullByDefault({}) ScheduledFuture<?> heartbeatFuture;
60
61     public RuuviTagHandler(Thing thing) {
62         super(thing);
63     }
64
65     @Override
66     public void initialize() {
67         super.initialize();
68         if (getThing().getStatus() != ThingStatus.OFFLINE) {
69             heartbeatFuture = scheduler.scheduleWithFixedDelay(this::heartbeat, 0, HEARTBEAT_TIMEOUT_MINUTES,
70                     TimeUnit.MINUTES);
71         }
72     }
73
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");
81             }
82         }
83     }
84
85     @Override
86     public void dispose() {
87         try {
88             super.dispose();
89         } finally {
90             if (heartbeatFuture != null) {
91                 heartbeatFuture.cancel(true);
92                 heartbeatFuture = null;
93             }
94         }
95     }
96
97     @Override
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(),
106                         ruuvitagData);
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);
115                                 break;
116                             case CHANNEL_ID_ACCELERATIONY:
117                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
118                                         ruuvitagData.getAccelerationY(), Units.STANDARD_GRAVITY);
119                                 break;
120                             case CHANNEL_ID_ACCELERATIONZ:
121                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
122                                         ruuvitagData.getAccelerationZ(), Units.STANDARD_GRAVITY);
123                                 break;
124                             case CHANNEL_ID_BATTERY:
125                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
126                                         ruuvitagData.getBatteryVoltage(), Units.VOLT);
127                                 break;
128                             case CHANNEL_ID_DATA_FORMAT:
129                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
130                                         ruuvitagData.getDataFormat());
131                                 break;
132                             case CHANNEL_ID_HUMIDITY:
133                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
134                                         ruuvitagData.getHumidity(), Units.PERCENT);
135                                 break;
136                             case CHANNEL_ID_MEASUREMENT_SEQUENCE_NUMBER:
137                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
138                                         ruuvitagData.getMeasurementSequenceNumber(), Units.ONE);
139                                 break;
140                             case CHANNEL_ID_MOVEMENT_COUNTER:
141                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
142                                         ruuvitagData.getMovementCounter(), Units.ONE);
143                                 break;
144                             case CHANNEL_ID_PRESSURE:
145                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
146                                         ruuvitagData.getPressure(), SIUnits.PASCAL);
147                                 break;
148                             case CHANNEL_ID_TEMPERATURE:
149                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
150                                         ruuvitagData.getTemperature(), SIUnits.CELSIUS);
151                                 break;
152                             case CHANNEL_ID_TX_POWER:
153                                 atLeastOneRuuviFieldPresent |= updateStateIfLinked(channelUID,
154                                         ruuvitagData.getTxPower(), Units.DECIBEL_MILLIWATTS);
155                                 break;
156                         }
157                     }
158                     if (atLeastOneRuuviFieldPresent) {
159                         // In practice, updated to ONLINE by super.onScanRecordReceived already, based on RSSI value
160                     } else {
161                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162                                 "Received Ruuvi Tag data but no fields could be parsed");
163                     }
164                 } else {
165                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
166                             "Received bluetooth data which could not be parsed to any known Ruuvi Tag data formats");
167                 }
168             } else {
169                 // Received Bluetooth scan with no manufacturer data
170                 // This happens -- we ignore this silently.
171             }
172         }
173     }
174
175     /**
176      * Update QuantityType channel state
177      *
178      * Update is not done when value is null.
179      *
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
184      */
185     private <T extends Quantity<T>> boolean updateStateIfLinked(ChannelUID channelUID, @Nullable Number value,
186             Unit<T> unit) {
187         if (value == null) {
188             return false;
189         }
190         if (isLinked(channelUID)) {
191             updateState(channelUID, new QuantityType<>(value, unit));
192         }
193         return true;
194     }
195
196     /**
197      * Update DecimalType channel state
198      *
199      * Update is not done when value is null.
200      *
201      * @param channelUID channel UID
202      * @param value value to update
203      * @return whether the value was present
204      */
205     private <T extends Quantity<T>> boolean updateStateIfLinked(ChannelUID channelUID, @Nullable Integer value) {
206         if (value == null) {
207             return false;
208         }
209         if (isLinked(channelUID)) {
210             updateState(channelUID, new DecimalType(value));
211         }
212         return true;
213     }
214 }