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.am43.internal;
15 import java.util.concurrent.Executor;
16 import java.util.concurrent.ExecutorService;
17 import java.util.concurrent.Executors;
18 import java.util.concurrent.ScheduledFuture;
19 import java.util.concurrent.TimeUnit;
21 import javax.measure.quantity.Length;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
26 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
27 import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
28 import org.openhab.binding.bluetooth.am43.internal.command.AM43Command;
29 import org.openhab.binding.bluetooth.am43.internal.command.ControlCommand;
30 import org.openhab.binding.bluetooth.am43.internal.command.GetAllCommand;
31 import org.openhab.binding.bluetooth.am43.internal.command.GetBatteryLevelCommand;
32 import org.openhab.binding.bluetooth.am43.internal.command.GetLightLevelCommand;
33 import org.openhab.binding.bluetooth.am43.internal.command.GetPositionCommand;
34 import org.openhab.binding.bluetooth.am43.internal.command.GetSpeedCommand;
35 import org.openhab.binding.bluetooth.am43.internal.command.ResponseListener;
36 import org.openhab.binding.bluetooth.am43.internal.command.SetPositionCommand;
37 import org.openhab.binding.bluetooth.am43.internal.command.SetSettingsCommand;
38 import org.openhab.binding.bluetooth.am43.internal.data.ControlAction;
39 import org.openhab.binding.bluetooth.am43.internal.data.Direction;
40 import org.openhab.binding.bluetooth.am43.internal.data.MotorSettings;
41 import org.openhab.binding.bluetooth.am43.internal.data.OperationMode;
42 import org.openhab.core.common.NamedThreadFactory;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.OnOffType;
45 import org.openhab.core.library.types.PercentType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.types.StopMoveType;
48 import org.openhab.core.library.types.StringType;
49 import org.openhab.core.library.types.UpDownType;
50 import org.openhab.core.library.unit.MetricPrefix;
51 import org.openhab.core.library.unit.SIUnits;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.types.Command;
55 import org.openhab.core.types.RefreshType;
56 import org.openhab.core.types.State;
57 import org.openhab.core.types.UnDefType;
58 import org.openhab.core.util.HexUtils;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
63 * The {@link AM43Handler} is responsible for handling commands, which are
64 * sent to one of the channels.
66 * @author Connor Petty - Initial contribution
69 public class AM43Handler extends ConnectedBluetoothHandler implements ResponseListener {
71 private final Logger logger = LoggerFactory.getLogger(AM43Handler.class);
73 private @Nullable AM43Configuration config;
75 private volatile @Nullable AM43Command currentCommand = null;
77 private @Nullable ScheduledFuture<?> refreshJob;
79 private @Nullable ExecutorService commandExecutor;
81 private @Nullable MotorSettings motorSettings = null;
83 public AM43Handler(Thing thing) {
88 public void initialize() {
90 config = getConfigAs(AM43Configuration.class);
92 commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
93 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
94 submitCommand(new GetAllCommand());
95 submitCommand(new GetBatteryLevelCommand());
96 submitCommand(new GetLightLevelCommand());
97 }, 10, getAM43Config().refreshInterval, TimeUnit.SECONDS);
101 public void dispose() {
102 dispose(commandExecutor);
103 dispose(currentCommand);
106 commandExecutor = null;
107 currentCommand = null;
109 motorSettings = null;
113 private static void dispose(@Nullable ExecutorService executor) {
114 if (executor != null) {
115 executor.shutdownNow();
119 private static void dispose(@Nullable ScheduledFuture<?> future) {
120 if (future != null) {
125 private static void dispose(@Nullable AM43Command command) {
126 if (command != null) {
127 // even if it already completed it doesn't really matter.
128 // on the off chance that the commandExecutor is waiting on the command, we can wake it up and cause it to
130 command.setState(AM43Command.State.FAILED);
134 private MotorSettings getMotorSettings() {
135 MotorSettings settings = motorSettings;
136 if (settings == null) {
137 throw new IllegalStateException("motorSettings has not been initialized");
142 private AM43Configuration getAM43Config() {
143 AM43Configuration ret = config;
145 throw new IllegalStateException("config has not been initialized");
150 private void submitCommand(AM43Command command) {
151 Executor executor = commandExecutor;
152 if (executor != null) {
153 executor.execute(() -> processCommand(command));
157 private void processCommand(AM43Command command) {
159 currentCommand = command;
160 if (device.getConnectionState() != ConnectionState.CONNECTED) {
161 logger.debug("Unable to send command {} to device {}: not connected", command, device.getAddress());
162 command.setState(AM43Command.State.FAILED);
165 if (!device.isServicesDiscovered()) {
166 logger.debug("Unable to send command {} to device {}: services not resolved", command,
167 device.getAddress());
168 command.setState(AM43Command.State.FAILED);
171 BluetoothCharacteristic characteristic = device.getCharacteristic(AM43BindingConstants.CHARACTERISTIC_UUID);
172 if (characteristic == null) {
173 logger.warn("Unable to execute {}. Characteristic '{}' could not be found.", command,
174 AM43BindingConstants.CHARACTERISTIC_UUID);
175 command.setState(AM43Command.State.FAILED);
178 // there is no consequence to calling this as much as we like
179 device.enableNotifications(characteristic);
181 command.setState(AM43Command.State.ENQUEUED);
182 device.writeCharacteristic(characteristic, command.getRequest()).whenComplete((v, t) -> {
184 logger.debug("Failed to send command {}: {}", command.getClass().getSimpleName(), t.getMessage());
185 command.setState(AM43Command.State.FAILED);
187 command.setState(AM43Command.State.SENT);
191 if (!command.awaitStateChange(getAM43Config().commandTimeout, TimeUnit.MILLISECONDS,
192 AM43Command.State.SUCCEEDED, AM43Command.State.FAILED)) {
193 logger.debug("Command {} to device {} timed out", command, device.getAddress());
195 } catch (InterruptedException e) {
198 logger.trace("Command final state: {}", command.getState());
199 currentCommand = null;
204 public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] response) {
205 super.onCharacteristicUpdate(characteristic, response);
207 AM43Command command = currentCommand;
208 if (command == null) {
209 if (logger.isDebugEnabled()) {
210 logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response));
212 } else if (!command.handleResponse(scheduler, this, response)) {
213 if (logger.isDebugEnabled()) {
214 logger.debug("Command {} could not handle response {}", command, HexUtils.bytesToHex(response));
220 public void handleCommand(ChannelUID channelUID, Command command) {
221 if (command instanceof RefreshType) {
222 switch (channelUID.getId()) {
223 case AM43BindingConstants.CHANNEL_ID_ELECTRIC:
224 submitCommand(new GetBatteryLevelCommand());
226 case AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL:
227 submitCommand(new GetLightLevelCommand());
229 case AM43BindingConstants.CHANNEL_ID_POSITION:
230 submitCommand(new GetPositionCommand());
233 submitCommand(new GetAllCommand());
236 switch (channelUID.getId()) {
237 case AM43BindingConstants.CHANNEL_ID_POSITION:
238 if (command instanceof PercentType percentCommand) {
239 MotorSettings settings = motorSettings;
240 if (settings == null) {
241 logger.warn("Cannot set position before settings have been received.");
244 if (!settings.isTopLimitSet() || !settings.isBottomLimitSet()) {
246 Cannot set position of blinds. Top or bottom limits have not been set. \
247 Please configure manually.\
251 int value = percentCommand.intValue();
252 if (getAM43Config().invertPosition) {
255 submitCommand(new SetPositionCommand(value));
258 if (command instanceof StopMoveType stopMoveCommand) {
259 switch (stopMoveCommand) {
261 submitCommand(new ControlCommand(ControlAction.STOP));
268 if (command instanceof UpDownType upDownCommand) {
269 switch (upDownCommand) {
271 submitCommand(new ControlCommand(ControlAction.OPEN));
274 submitCommand(new ControlCommand(ControlAction.CLOSE));
279 case AM43BindingConstants.CHANNEL_ID_SPEED:
280 if (command instanceof DecimalType decimalCommand) {
281 MotorSettings settings = motorSettings;
282 if (settings != null) {
283 settings.setSpeed(decimalCommand.intValue());
284 submitCommand(new SetSettingsCommand(settings));
286 logger.warn("Cannot set Speed before setting have been received");
290 case AM43BindingConstants.CHANNEL_ID_DIRECTION:
291 if (command instanceof StringType) {
292 MotorSettings settings = motorSettings;
293 if (settings != null) {
294 settings.setDirection(Direction.valueOf(command.toString()));
295 submitCommand(new SetSettingsCommand(settings));
297 logger.warn("Cannot set Direction before setting have been received");
301 case AM43BindingConstants.CHANNEL_ID_OPERATION_MODE:
302 if (command instanceof StringType) {
303 MotorSettings settings = motorSettings;
304 if (settings != null) {
305 settings.setOperationMode(OperationMode.valueOf(command.toString()));
306 submitCommand(new SetSettingsCommand(settings));
308 logger.warn("Cannot set OperationMode before setting have been received");
314 super.handleCommand(channelUID, command);
318 public void receivedResponse(GetLightLevelCommand command) {
319 updateLightLevel(command.getLightLevel());
323 public void receivedResponse(GetPositionCommand command) {
324 updatePosition(command.getPosition());
328 public void receivedResponse(GetSpeedCommand command) {
329 getMotorSettings().setSpeed(command.getSpeed());
330 updateSpeed(command.getSpeed());
334 public void receivedResponse(GetAllCommand command) {
335 motorSettings = new MotorSettings();
337 updateDirection(command.getDirection());
338 updateOperationMode(command.getOperationMode());
339 updateTopLimitSet(command.getTopLimitSet());
340 updateBottomLimitSet(command.getBottomLimitSet());
341 updateHasLightSensor(command.getHasLightSensor());
342 updateSpeed(command.getSpeed());
343 updatePosition(command.getPosition());
344 updateLength(command.getLength());
345 updateDiameter(command.getDiameter());
346 updateType(command.getType());
350 public void receivedResponse(GetBatteryLevelCommand command) {
351 updateBatteryLevel(command.getBatteryLevel());
354 private void updateDirection(Direction direction) {
355 getMotorSettings().setDirection(direction);
357 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIRECTION, new StringType(direction.toString()));
360 private void updateOperationMode(OperationMode opMode) {
361 getMotorSettings().setOperationMode(opMode);
363 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(opMode.toString()));
366 private void updateTopLimitSet(boolean bitValue) {
367 getMotorSettings().setTopLimitSet(bitValue);
369 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TOP_LIMIT_SET, OnOffType.from(bitValue));
372 private void updateBottomLimitSet(boolean bitValue) {
373 getMotorSettings().setBottomLimitSet(bitValue);
375 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_BOTTOM_LIMIT_SET, OnOffType.from(bitValue));
378 private void updateHasLightSensor(boolean bitValue) {
379 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_HAS_LIGHT_SENSOR, OnOffType.from(bitValue));
382 private void updateSpeed(int value) {
383 getMotorSettings().setSpeed(value);
385 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_SPEED, new DecimalType(value));
388 private void updatePosition(int value) {
389 if (value >= 0 && value <= 100) {
390 int percentValue = value;
391 if (getAM43Config().invertPosition) {
392 percentValue = 100 - percentValue;
394 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, new PercentType(percentValue));
396 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, UnDefType.UNDEF);
400 private void updateLength(int value) {
401 getMotorSettings().setLength(value);
403 QuantityType<Length> lengthType = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
404 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LENGTH, lengthType);
407 private void updateDiameter(int value) {
408 getMotorSettings().setDiameter(value);
410 QuantityType<Length> diameter = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
411 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIAMETER, diameter);
414 private void updateType(int value) {
415 getMotorSettings().setType(value);
417 DecimalType type = new DecimalType(value);
418 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TYPE, type);
421 private void updateLightLevel(int value) {
422 DecimalType lightLevel = new DecimalType(value);
423 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL, lightLevel);
426 private void updateBatteryLevel(int value) {
427 if (value >= 0 && value <= 100) {
428 DecimalType deviceElectric = new DecimalType(value & 0xFF);
429 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_ELECTRIC, deviceElectric);
431 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_ELECTRIC, UnDefType.UNDEF);
435 private void updateStateIfLinked(String channelUID, State state) {
436 if (isLinked(channelUID)) {
437 updateState(channelUID, state);