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.Optional;
16 import java.util.UUID;
17 import java.util.concurrent.ScheduledFuture;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.atomic.AtomicInteger;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.bluetooth.BeaconBluetoothHandler;
24 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
25 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
26 import org.openhab.binding.bluetooth.BluetoothUtils;
27 import org.openhab.binding.bluetooth.notification.BluetoothConnectionStatusNotification;
28 import org.openhab.core.thing.Thing;
29 import org.openhab.core.thing.ThingStatus;
30 import org.openhab.core.thing.ThingStatusDetail;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
35 * The {@link AbstractRadoneyeHandler} is responsible for handling commands, which are
36 * sent to one of the channels.
38 * @author Peter Obel - Initial contribution
41 abstract public class AbstractRadoneyeHandler extends BeaconBluetoothHandler {
43 private static final int CHECK_PERIOD_SEC = 10;
45 private final Logger logger = LoggerFactory.getLogger(AbstractRadoneyeHandler.class);
47 private AtomicInteger sinceLastReadSec = new AtomicInteger();
48 private Optional<RadoneyeConfiguration> configuration = Optional.empty();
49 private @Nullable ScheduledFuture<?> scheduledTask;
51 private volatile int refreshInterval;
52 private volatile int errorConnectCounter;
53 private volatile int errorReadCounter;
54 private volatile int errorWriteCounter;
55 private volatile int errorDisconnectCounter;
56 private volatile int errorResolvingCounter;
58 private volatile ServiceState serviceState = ServiceState.NOT_RESOLVED;
59 private volatile ReadState readState = ReadState.IDLE;
61 private enum ServiceState {
67 private enum ReadState {
73 public AbstractRadoneyeHandler(Thing thing) {
78 public void initialize() {
79 logger.debug("Initialize");
81 configuration = Optional.of(getConfigAs(RadoneyeConfiguration.class));
82 logger.debug("Using configuration: {}", configuration.get());
83 cancelScheduledTask();
84 configuration.ifPresent(cfg -> {
85 refreshInterval = cfg.refreshInterval;
86 logger.debug("Start scheduled task to read device in every {} seconds", refreshInterval);
87 scheduledTask = scheduler.scheduleWithFixedDelay(this::executePeridioc, CHECK_PERIOD_SEC, CHECK_PERIOD_SEC,
90 sinceLastReadSec.set(refreshInterval); // update immediately
94 public void dispose() {
95 logger.debug("Dispose");
96 cancelScheduledTask();
97 serviceState = ServiceState.NOT_RESOLVED;
98 readState = ReadState.IDLE;
102 private void cancelScheduledTask() {
103 if (scheduledTask != null) {
104 scheduledTask.cancel(true);
105 scheduledTask = null;
109 private void executePeridioc() {
110 sinceLastReadSec.addAndGet(CHECK_PERIOD_SEC);
114 private synchronized void execute() {
115 ConnectionState connectionState = device.getConnectionState();
116 logger.debug("Device {} state is {}, serviceState {}, readState {}", address, connectionState, serviceState,
119 switch (connectionState) {
123 if (isTimeToRead()) {
135 private void connect() {
136 logger.debug("Connect to device {}...", address);
137 if (!device.connect()) {
138 errorConnectCounter++;
139 if (errorConnectCounter < 6) {
140 logger.debug("Connecting to device {} failed {} times", address, errorConnectCounter);
142 logger.debug("ERROR: Controller reset needed. Connecting to device {} failed {} times", address,
143 errorConnectCounter);
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connecting to device failed");
147 logger.debug("Connected to device {}", address);
148 errorConnectCounter = 0;
152 private void disconnect() {
153 logger.debug("Disconnect from device {}...", address);
154 if (!device.disconnect()) {
155 errorDisconnectCounter++;
156 if (errorDisconnectCounter < 6) {
157 logger.debug("Disconnect from device {} failed {} times", address, errorDisconnectCounter);
159 logger.debug("ERROR: Controller reset needed. Disconnect from device {} failed {} times", address,
160 errorDisconnectCounter);
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162 "Disconnect from device failed");
165 logger.debug("Disconnected from device {}", address);
166 errorDisconnectCounter = 0;
170 private void read() {
171 switch (serviceState) {
173 logger.debug("Discover services on device {}", address);
179 if (getTriggerUUID() != null) {
180 logger.debug("Send trigger data to device {}...", address);
181 BluetoothCharacteristic characteristic = device.getCharacteristic(getTriggerUUID());
182 if (characteristic != null) {
183 readState = ReadState.WRITING;
184 errorWriteCounter = 0;
185 device.writeCharacteristic(characteristic, getTriggerData()).whenComplete((v, ex) -> {
190 if (errorWriteCounter < 6) {
191 logger.debug("Read/write data from device {} failed {} times", address,
195 "ERROR: Controller reset needed. Read/write data from device {} failed {} times",
196 address, errorWriteCounter);
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
198 "Read/write data from device failed");
208 logger.debug("Unhandled Resolved readState {} on device {}", readState, address);
212 default: // serviceState RESOLVING
213 errorResolvingCounter++;
214 if (errorResolvingCounter < 6) {
215 logger.debug("Unhandled serviceState {} on device {}", serviceState, address);
217 logger.debug("ERROR: Controller reset needed. Unhandled serviceState {} on device {}",
218 serviceState, address);
219 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
220 "Service discovery for device failed");
226 private void readSensorData() {
227 logger.debug("Read data from device {}...", address);
228 BluetoothCharacteristic characteristic = device.getCharacteristic(getDataUUID());
229 if (characteristic != null) {
230 readState = ReadState.READING;
231 errorReadCounter = 0;
232 errorResolvingCounter = 0;
233 device.readCharacteristic(characteristic).whenComplete((data, ex) -> {
235 logger.debug("Characteristic {} from device {}: {}", characteristic.getUuid(), address, data);
236 updateStatus(ThingStatus.ONLINE);
237 sinceLastReadSec.set(0);
238 updateChannels(BluetoothUtils.toIntArray(data));
240 readState = ReadState.IDLE;
246 if (errorReadCounter < 6) {
247 logger.debug("Read data from device {} failed {} times", address, errorReadCounter);
249 logger.debug("ERROR: Controller reset needed. Read data from device {} failed {} times", address,
251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
252 "Read data from device failed");
258 private void discoverServices() {
259 logger.debug("Discover services for device {}", address);
260 serviceState = ServiceState.RESOLVING;
261 device.discoverServices();
265 public void onServicesDiscovered() {
266 serviceState = ServiceState.RESOLVED;
267 logger.debug("Service discovery completed for device {}", address);
272 private void printServices() {
273 device.getServices().forEach(service -> logger.debug("Device {} Service '{}'", address, service));
277 public void onConnectionStateChange(BluetoothConnectionStatusNotification connectionNotification) {
278 logger.debug("Connection State Change Event is {}", connectionNotification.getConnectionState());
279 switch (connectionNotification.getConnectionState()) {
281 if (serviceState == ServiceState.RESOLVING) {
282 serviceState = ServiceState.NOT_RESOLVED;
284 readState = ReadState.IDLE;
293 private boolean isTimeToRead() {
294 int sinceLastRead = sinceLastReadSec.get();
295 logger.debug("Time since last update: {} sec", sinceLastRead);
296 return sinceLastRead >= refreshInterval;
300 * Provides the UUID of the characteristic, which holds the sensor data
302 * @return the UUID of the data characteristic
304 protected abstract UUID getDataUUID();
307 * Provides the UUID of the characteristic, that triggers and update of the sensor data
309 * @return the UUID of the data characteristic
311 protected abstract UUID getTriggerUUID();
314 * Provides the data that sent to the trigger characteristic will update the sensor data
316 * @return the trigger data as an byte array
318 protected abstract byte[] getTriggerData();
321 * This method parses the content of the bluetooth characteristic and updates the Thing channels accordingly.
323 * @param is the content of the bluetooth characteristic
325 abstract protected void updateChannels(int[] is);