2 * Copyright (c) 2010-2021 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.Arrays;
16 import java.util.concurrent.Executor;
17 import java.util.concurrent.ExecutorService;
18 import java.util.concurrent.Executors;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
22 import javax.measure.quantity.Length;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
27 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
28 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
29 import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
30 import org.openhab.binding.bluetooth.am43.internal.command.AM43Command;
31 import org.openhab.binding.bluetooth.am43.internal.command.ControlCommand;
32 import org.openhab.binding.bluetooth.am43.internal.command.GetAllCommand;
33 import org.openhab.binding.bluetooth.am43.internal.command.GetBatteryLevelCommand;
34 import org.openhab.binding.bluetooth.am43.internal.command.GetLightLevelCommand;
35 import org.openhab.binding.bluetooth.am43.internal.command.GetPositionCommand;
36 import org.openhab.binding.bluetooth.am43.internal.command.GetSpeedCommand;
37 import org.openhab.binding.bluetooth.am43.internal.command.ResponseListener;
38 import org.openhab.binding.bluetooth.am43.internal.command.SetPositionCommand;
39 import org.openhab.binding.bluetooth.am43.internal.command.SetSettingsCommand;
40 import org.openhab.binding.bluetooth.am43.internal.data.ControlAction;
41 import org.openhab.binding.bluetooth.am43.internal.data.Direction;
42 import org.openhab.binding.bluetooth.am43.internal.data.MotorSettings;
43 import org.openhab.binding.bluetooth.am43.internal.data.OperationMode;
44 import org.openhab.core.common.NamedThreadFactory;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.OnOffType;
47 import org.openhab.core.library.types.PercentType;
48 import org.openhab.core.library.types.QuantityType;
49 import org.openhab.core.library.types.StopMoveType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.library.types.UpDownType;
52 import org.openhab.core.library.unit.MetricPrefix;
53 import org.openhab.core.library.unit.SIUnits;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.RefreshType;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.UnDefType;
60 import org.openhab.core.util.HexUtils;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
65 * The {@link AM43Handler} is responsible for handling commands, which are
66 * sent to one of the channels.
68 * @author Connor Petty - Initial contribution
71 public class AM43Handler extends ConnectedBluetoothHandler implements ResponseListener {
73 private final Logger logger = LoggerFactory.getLogger(AM43Handler.class);
75 private @Nullable AM43Configuration config;
77 private volatile @Nullable AM43Command currentCommand = null;
79 private @Nullable ScheduledFuture<?> refreshJob;
81 private @Nullable ExecutorService commandExecutor;
83 private @Nullable MotorSettings motorSettings = null;
85 public AM43Handler(Thing thing) {
90 public void initialize() {
92 config = getConfigAs(AM43Configuration.class);
94 commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
95 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
96 submitCommand(new GetAllCommand());
97 submitCommand(new GetBatteryLevelCommand());
98 submitCommand(new GetLightLevelCommand());
99 }, 10, getAM43Config().refreshInterval, TimeUnit.SECONDS);
103 public void dispose() {
104 dispose(commandExecutor);
105 dispose(currentCommand);
108 commandExecutor = null;
109 currentCommand = null;
111 motorSettings = null;
115 private static void dispose(@Nullable ExecutorService executor) {
116 if (executor != null) {
117 executor.shutdownNow();
121 private static void dispose(@Nullable ScheduledFuture<?> future) {
122 if (future != null) {
127 private static void dispose(@Nullable AM43Command command) {
128 if (command != null) {
129 // even if it already completed it doesn't really matter.
130 // on the off chance that the commandExecutor is waiting on the command, we can wake it up and cause it to
132 command.setState(AM43Command.State.FAILED);
136 private MotorSettings getMotorSettings() {
137 MotorSettings settings = motorSettings;
138 if (settings == null) {
139 throw new IllegalStateException("motorSettings has not been initialized");
144 private AM43Configuration getAM43Config() {
145 AM43Configuration ret = config;
147 throw new IllegalStateException("config has not been initialized");
152 private void submitCommand(AM43Command command) {
153 Executor executor = commandExecutor;
154 if (executor != null) {
155 executor.execute(() -> processCommand(command));
159 private void processCommand(AM43Command command) {
161 currentCommand = command;
162 if (device.getConnectionState() != ConnectionState.CONNECTED) {
163 logger.debug("Unable to send command {} to device {}: not connected", command, device.getAddress());
164 command.setState(AM43Command.State.FAILED);
168 logger.debug("Unable to send command {} to device {}: services not resolved", command,
169 device.getAddress());
170 command.setState(AM43Command.State.FAILED);
173 BluetoothCharacteristic characteristic = device.getCharacteristic(AM43BindingConstants.CHARACTERISTIC_UUID);
174 if (characteristic == null) {
175 logger.warn("Unable to execute {}. Characteristic '{}' could not be found.", command,
176 AM43BindingConstants.CHARACTERISTIC_UUID);
177 command.setState(AM43Command.State.FAILED);
180 // there is no consequence to calling this as much as we like
181 device.enableNotifications(characteristic);
183 characteristic.setValue(command.getRequest());
184 command.setState(AM43Command.State.ENQUEUED);
185 device.writeCharacteristic(characteristic);
187 if (!command.awaitStateChange(getAM43Config().commandTimeout, TimeUnit.MILLISECONDS,
188 AM43Command.State.SUCCEEDED, AM43Command.State.FAILED)) {
189 logger.debug("Command {} to device {} timed out", command, device.getAddress());
191 } catch (InterruptedException e) {
194 logger.trace("Command final state: {}", command.getState());
195 currentCommand = null;
200 public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
201 BluetoothCompletionStatus status) {
202 super.onCharacteristicWriteComplete(characteristic, status);
204 byte[] request = characteristic.getByteValue();
206 AM43Command command = currentCommand;
208 if (command != null) {
209 if (!Arrays.equals(request, command.getRequest())) {
210 logger.debug("Write completed for unknown command");
215 command.setState(AM43Command.State.SENT);
218 command.setState(AM43Command.State.FAILED);
222 if (logger.isDebugEnabled()) {
223 logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
229 public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
230 super.onCharacteristicUpdate(characteristic);
232 byte[] response = characteristic.getByteValue();
234 AM43Command command = currentCommand;
235 if (command == null) {
236 if (logger.isDebugEnabled()) {
237 logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response));
239 } else if (!command.handleResponse(scheduler, this, response)) {
240 if (logger.isDebugEnabled()) {
241 logger.debug("Command {} could not handle response {}", command, HexUtils.bytesToHex(response));
247 public void handleCommand(ChannelUID channelUID, Command command) {
248 if (command instanceof RefreshType) {
249 switch (channelUID.getId()) {
250 case AM43BindingConstants.CHANNEL_ID_ELECTRIC:
251 submitCommand(new GetBatteryLevelCommand());
253 case AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL:
254 submitCommand(new GetLightLevelCommand());
256 case AM43BindingConstants.CHANNEL_ID_POSITION:
257 submitCommand(new GetPositionCommand());
260 submitCommand(new GetAllCommand());
263 switch (channelUID.getId()) {
264 case AM43BindingConstants.CHANNEL_ID_POSITION:
265 if (command instanceof PercentType) {
266 MotorSettings settings = motorSettings;
267 if (settings == null) {
268 logger.warn("Cannot set position before settings have been received.");
271 if (!settings.isTopLimitSet() || !settings.isBottomLimitSet()) {
272 logger.warn("Cannot set position of blinds. Top or bottom limits have not been set. "
273 + "Please configure manually.");
276 PercentType percent = (PercentType) command;
277 int value = percent.intValue();
278 if (getAM43Config().invertPosition) {
281 submitCommand(new SetPositionCommand(value));
284 if (command instanceof StopMoveType) {
285 switch ((StopMoveType) command) {
287 submitCommand(new ControlCommand(ControlAction.STOP));
294 if (command instanceof UpDownType) {
295 switch ((UpDownType) command) {
297 submitCommand(new ControlCommand(ControlAction.OPEN));
300 submitCommand(new ControlCommand(ControlAction.CLOSE));
305 case AM43BindingConstants.CHANNEL_ID_SPEED:
306 if (command instanceof DecimalType) {
307 MotorSettings settings = motorSettings;
308 if (settings != null) {
309 DecimalType speedType = (DecimalType) command;
310 settings.setSpeed(speedType.intValue());
311 submitCommand(new SetSettingsCommand(settings));
313 logger.warn("Cannot set Speed before setting have been received");
317 case AM43BindingConstants.CHANNEL_ID_DIRECTION:
318 if (command instanceof StringType) {
319 MotorSettings settings = motorSettings;
320 if (settings != null) {
321 settings.setDirection(Direction.valueOf(command.toString()));
322 submitCommand(new SetSettingsCommand(settings));
324 logger.warn("Cannot set Direction before setting have been received");
328 case AM43BindingConstants.CHANNEL_ID_OPERATION_MODE:
329 if (command instanceof StringType) {
330 MotorSettings settings = motorSettings;
331 if (settings != null) {
332 settings.setOperationMode(OperationMode.valueOf(command.toString()));
333 submitCommand(new SetSettingsCommand(settings));
335 logger.warn("Cannot set OperationMode before setting have been received");
341 super.handleCommand(channelUID, command);
345 public void receivedResponse(GetLightLevelCommand command) {
346 updateLightLevel(command.getLightLevel());
350 public void receivedResponse(GetPositionCommand command) {
351 updatePosition(command.getPosition());
355 public void receivedResponse(GetSpeedCommand command) {
356 getMotorSettings().setSpeed(command.getSpeed());
357 updateSpeed(command.getSpeed());
361 public void receivedResponse(GetAllCommand command) {
362 motorSettings = new MotorSettings();
364 updateDirection(command.getDirection());
365 updateOperationMode(command.getOperationMode());
366 updateTopLimitSet(command.getTopLimitSet());
367 updateBottomLimitSet(command.getBottomLimitSet());
368 updateHasLightSensor(command.getHasLightSensor());
369 updateSpeed(command.getSpeed());
370 updatePosition(command.getPosition());
371 updateLength(command.getLength());
372 updateDiameter(command.getDiameter());
373 updateType(command.getType());
377 public void receivedResponse(GetBatteryLevelCommand command) {
378 updateBatteryLevel(command.getBatteryLevel());
381 private void updateDirection(Direction direction) {
382 getMotorSettings().setDirection(direction);
384 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIRECTION, new StringType(direction.toString()));
387 private void updateOperationMode(OperationMode opMode) {
388 getMotorSettings().setOperationMode(opMode);
390 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(opMode.toString()));
393 private void updateTopLimitSet(boolean bitValue) {
394 getMotorSettings().setTopLimitSet(bitValue);
396 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TOP_LIMIT_SET, OnOffType.from(bitValue));
399 private void updateBottomLimitSet(boolean bitValue) {
400 getMotorSettings().setBottomLimitSet(bitValue);
402 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_BOTTOM_LIMIT_SET, OnOffType.from(bitValue));
405 private void updateHasLightSensor(boolean bitValue) {
406 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_HAS_LIGHT_SENSOR, OnOffType.from(bitValue));
409 private void updateSpeed(int value) {
410 getMotorSettings().setSpeed(value);
412 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_SPEED, new DecimalType(value));
415 private void updatePosition(int value) {
416 if (value >= 0 && value <= 100) {
417 int percentValue = value;
418 if (getAM43Config().invertPosition) {
419 percentValue = 100 - percentValue;
421 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, new PercentType(percentValue));
423 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, UnDefType.UNDEF);
427 private void updateLength(int value) {
428 getMotorSettings().setLength(value);
430 QuantityType<Length> lengthType = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
431 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LENGTH, lengthType);
434 private void updateDiameter(int value) {
435 getMotorSettings().setDiameter(value);
437 QuantityType<Length> diameter = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
438 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIAMETER, diameter);
441 private void updateType(int value) {
442 getMotorSettings().setType(value);
444 DecimalType type = new DecimalType(value);
445 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TYPE, type);
448 private void updateLightLevel(int value) {
449 DecimalType lightLevel = new DecimalType(value);
450 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL, lightLevel);
453 private void updateBatteryLevel(int value) {
454 if (value >= 0 && value <= 100) {
455 DecimalType deviceElectric = new DecimalType(value & 0xFF);
456 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_ELECTRIC, deviceElectric);
458 updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_ELECTRIC, UnDefType.UNDEF);
462 private void updateStateIfLinked(String channelUID, State state) {
463 if (isLinked(channelUID)) {
464 updateState(channelUID, state);