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.radoneye.internal;
15 import java.util.UUID;
16 import java.util.concurrent.ScheduledFuture;
17 import java.util.concurrent.TimeUnit;
18 import java.util.concurrent.atomic.AtomicInteger;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
23 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
24 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
25 import org.openhab.binding.bluetooth.BluetoothUtils;
26 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
27 import org.openhab.core.thing.Thing;
28 import org.openhab.core.thing.ThingStatus;
29 import org.openhab.core.thing.ThingStatusDetail;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
34 * The {@link AbstractRadoneyeHandler} is responsible for handling commands, which are
35 * sent to one of the channels.
37 * @author Peter Obel - Initial contribution
40 abstract public class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
42 private static final int CHECK_PERIOD_SEC = 10;
44 private final Logger logger = LoggerFactory.getLogger(AbstractRadoneyeHandler.class);
46 private AtomicInteger sinceLastReadSec = new AtomicInteger();
47 private RadoneyeConfiguration configuration = new RadoneyeConfiguration();
48 private @Nullable ScheduledFuture<?> scheduledTask;
50 private volatile int errorConnectCounter;
51 private volatile int errorReadCounter;
52 private volatile int errorWriteCounter;
53 private volatile int errorDisconnectCounter;
54 private volatile int errorResolvingCounter;
56 private volatile ServiceState serviceState = ServiceState.NOT_RESOLVED;
57 private volatile ReadState readState = ReadState.IDLE;
59 private enum ServiceState {
65 private enum ReadState {
71 public AbstractRadoneyeHandler(Thing thing) {
76 public void initialize() {
77 logger.debug("Initialize");
79 configuration = getConfigAs(RadoneyeConfiguration.class);
80 logger.debug("Using configuration: {}", configuration);
81 cancelScheduledTask();
82 logger.debug("Start scheduled task to read device in every {} seconds", configuration.refreshInterval);
83 scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
86 sinceLastReadSec.set(configuration.refreshInterval); // update immediately
90 public void dispose() {
91 logger.debug("Dispose");
92 cancelScheduledTask();
93 serviceState = ServiceState.NOT_RESOLVED;
94 readState = ReadState.IDLE;
98 private void cancelScheduledTask() {
99 if (scheduledTask != null) {
100 scheduledTask.cancel(true);
101 scheduledTask = null;
105 private void executePeridioc() {
106 sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
110 private synchronized void execute() {
111 ConnectionState connectionState = device.getConnectionState();
112 logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
115 switch (connectionState) {
119 if (isTimeToRead()) {
131 private void connect() {
132 logger.debug("Connect to device {}...", address);
133 if (!device.connect()) {
134 errorConnectCounter++;
135 if (errorConnectCounter < 6) {
136 logger.debug("Connecting to device {} failed {} times", address, errorConnectCounter);
138 logger.debug("ERROR: Controller reset needed. Connecting to device {} failed {} times", address,
139 errorConnectCounter);
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connecting to device failed");
143 logger.debug("Connected to device {}", address);
144 errorConnectCounter = 0;
148 private void disconnect() {
149 logger.debug("Disconnect from device {}...", address);
150 if (!device.disconnect()) {
151 errorDisconnectCounter++;
152 if (errorDisconnectCounter < 6) {
153 logger.debug("Disconnect from device {} failed {} times", address, errorDisconnectCounter);
155 logger.debug("ERROR: Controller reset needed. Disconnect from device {} failed {} times", address,
156 errorDisconnectCounter);
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
158 "Disconnect from device failed");
161 logger.debug("Disconnected from device {}", address);
162 errorDisconnectCounter = 0;
166 private void read() {
167 switch (serviceState) {
169 logger.debug("Discover services on device {}", address);
175 if (getTriggerUUID() != null) {
176 logger.debug("Send trigger data to device {}...", address);
177 BluetoothCharacteristic characteristic = device.getCharacteristic(getTriggerUUID());
178 if (characteristic != null) {
179 readState = ReadState.WRITING;
180 errorWriteCounter = 0;
181 device.writeCharacteristic(characteristic, getTriggerData()).whenComplete((v, ex) -> {
186 if (errorWriteCounter < 6) {
187 logger.debug("Read/write data from device {} failed {} times", address,
191 "ERROR: Controller reset needed. Read/write data from device {} failed {} times",
192 address, errorWriteCounter);
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194 "Read/write data from device failed");
204 logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
208 default: // serviceState RESOLVING
209 errorResolvingCounter++;
210 if (errorResolvingCounter < 6) {
211 logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
213 logger.debug("ERROR: Controller reset needed. Unhandled serviceState {} on device {}",
214 serviceState, address);
215 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
216 "Service discovery for device failed");
222 private void readSensorData() {
223 logger.debug("Read data from device {}...", address);
224 BluetoothCharacteristic characteristic = device.getCharacteristic(getDataUUID());
225 if (characteristic != null) {
226 readState = ReadState.READING;
227 errorReadCounter = 0;
228 errorResolvingCounter = 0;
229 device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
231 logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address, data);
232 updateStatus(ThingStatus.ONLINE);
233 sinceLastReadSec.set(0);
234 updateChannels(BluetoothUtils.toIntArray(data));
236 readState = ReadState.IDLE;
242 if (errorReadCounter < 6) {
243 logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
245 logger.debug("ERROR: Controller reset needed. Read data from device {} failed {} times", address,
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248 "Read data from device failed");
254 private void discoverServices() {
255 logger.debug("Discover services for device {}", address);
256 serviceState = ServiceState.RESOLVING;
257 device.discoverServices();
261 public void onServicesDiscovered() {
262 serviceState = ServiceState.RESOLVED;
263 logger.debug("Service discovery completed for device {}", address);
268 private void printServices() {
269 device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
273 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
274 logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
275 switch (connectionNotification.getConnectionState()) {
277 if (serviceState == ServiceState.RESOLVING) {
278 serviceState = ServiceState.NOT_RESOLVED;
280 readState = ReadState.IDLE;
289 private boolean isTimeToRead() {
290 int sinceLastRead = sinceLastReadSec.get();
291 logger.debug("Time since last update: {} sec", sinceLastRead);
292 return sinceLastRead >= configuration.refreshInterval;
296 * Provides the configured major firmware version
298 * @return the major firmware version configured
300 protected int getFwVersion() {
301 return configuration.fwVersion;
305 * Provides the UUID of the characteristic, which holds the sensor data
307 * @return the UUID of the data characteristic
309 protected abstract UUID getDataUUID();
312 * Provides the UUID of the characteristic, that triggers and update of the sensor data
314 * @return the UUID of the data characteristic
316 protected abstract UUID getTriggerUUID();
319 * Provides the data that sent to the trigger characteristic will update the sensor data
321 * @return the trigger data as an byte array
323 protected abstract byte[] getTriggerData();
326 * This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
328 * @param is the content of the bluetooth characteristic
330 abstract protected void updateChannels(int[] is);