]> git.basschouten.com Git - openhab-addons.git/blob
1bdcc7db64a3baf2f5b75a516979f0094d424676
[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.am43.internal;
14
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;
20
21 import javax.measure.quantity.Length;
22
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;
61
62 /**
63  * The {@link AM43Handler} is responsible for handling commands, which are
64  * sent to one of the channels.
65  *
66  * @author Connor Petty - Initial contribution
67  */
68 @NonNullByDefault
69 public class AM43Handler extends ConnectedBluetoothHandler implements ResponseListener {
70
71     private final Logger logger = LoggerFactory.getLogger(AM43Handler.class);
72
73     private @Nullable AM43Configuration config;
74
75     private volatile @Nullable AM43Command currentCommand = null;
76
77     private @Nullable ScheduledFuture<?> refreshJob;
78
79     private @Nullable ExecutorService commandExecutor;
80
81     private @Nullable MotorSettings motorSettings = null;
82
83     public AM43Handler(Thing thing) {
84         super(thing);
85     }
86
87     @Override
88     public void initialize() {
89         super.initialize();
90         config = getConfigAs(AM43Configuration.class);
91
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);
98     }
99
100     @Override
101     public void dispose() {
102         dispose(commandExecutor);
103         dispose(currentCommand);
104         dispose(refreshJob);
105
106         commandExecutor = null;
107         currentCommand = null;
108         refreshJob = null;
109         motorSettings = null;
110         super.dispose();
111     }
112
113     private static void dispose(@Nullable ExecutorService executor) {
114         if (executor != null) {
115             executor.shutdownNow();
116         }
117     }
118
119     private static void dispose(@Nullable ScheduledFuture<?> future) {
120         if (future != null) {
121             future.cancel(true);
122         }
123     }
124
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
129             // terminate
130             command.setState(AM43Command.State.FAILED);
131         }
132     }
133
134     private MotorSettings getMotorSettings() {
135         MotorSettings settings = motorSettings;
136         if (settings == null) {
137             throw new IllegalStateException("motorSettings has not been initialized");
138         }
139         return settings;
140     }
141
142     private AM43Configuration getAM43Config() {
143         AM43Configuration ret = config;
144         if (ret == null) {
145             throw new IllegalStateException("config has not been initialized");
146         }
147         return ret;
148     }
149
150     private void submitCommand(AM43Command command) {
151         Executor executor = commandExecutor;
152         if (executor != null) {
153             executor.execute(() -> processCommand(command));
154         }
155     }
156
157     private void processCommand(AM43Command command) {
158         try {
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);
163                 return;
164             }
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);
169                 return;
170             }
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);
176                 return;
177             }
178             // there is no consequence to calling this as much as we like
179             device.enableNotifications(characteristic);
180
181             command.setState(AM43Command.State.ENQUEUED);
182             device.writeCharacteristic(characteristic, command.getRequest()).whenComplete((v, t) -> {
183                 if (t != null) {
184                     logger.debug("Failed to send command {}: {}", command.getClass().getSimpleName(), t.getMessage());
185                     command.setState(AM43Command.State.FAILED);
186                 } else {
187                     command.setState(AM43Command.State.SENT);
188                 }
189             });
190
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());
194             }
195         } catch (InterruptedException e) {
196             // do nothing
197         } finally {
198             logger.trace("Command final state: {}", command.getState());
199             currentCommand = null;
200         }
201     }
202
203     @Override
204     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic, byte[] response) {
205         super.onCharacteristicUpdate(characteristic, response);
206
207         AM43Command command = currentCommand;
208         if (command == null) {
209             if (logger.isDebugEnabled()) {
210                 logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response));
211             }
212         } else if (!command.handleResponse(scheduler, this, response)) {
213             if (logger.isDebugEnabled()) {
214                 logger.debug("Command {} could not handle response {}", command, HexUtils.bytesToHex(response));
215             }
216         }
217     }
218
219     @Override
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());
225                     return;
226                 case AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL:
227                     submitCommand(new GetLightLevelCommand());
228                     return;
229                 case AM43BindingConstants.CHANNEL_ID_POSITION:
230                     submitCommand(new GetPositionCommand());
231                     return;
232             }
233             submitCommand(new GetAllCommand());
234             return;
235         }
236         switch (channelUID.getId()) {
237             case AM43BindingConstants.CHANNEL_ID_POSITION:
238                 if (command instanceof PercentType) {
239                     MotorSettings settings = motorSettings;
240                     if (settings == null) {
241                         logger.warn("Cannot set position before settings have been received.");
242                         return;
243                     }
244                     if (!settings.isTopLimitSet() || !settings.isBottomLimitSet()) {
245                         logger.warn("Cannot set position of blinds. Top or bottom limits have not been set. "
246                                 + "Please configure manually.");
247                         return;
248                     }
249                     PercentType percent = (PercentType) command;
250                     int value = percent.intValue();
251                     if (getAM43Config().invertPosition) {
252                         value = 100 - value;
253                     }
254                     submitCommand(new SetPositionCommand(value));
255                     return;
256                 }
257                 if (command instanceof StopMoveType) {
258                     switch ((StopMoveType) command) {
259                         case STOP:
260                             submitCommand(new ControlCommand(ControlAction.STOP));
261                             return;
262                         case MOVE:
263                             // do nothing
264                             return;
265                     }
266                 }
267                 if (command instanceof UpDownType) {
268                     switch ((UpDownType) command) {
269                         case UP:
270                             submitCommand(new ControlCommand(ControlAction.OPEN));
271                             return;
272                         case DOWN:
273                             submitCommand(new ControlCommand(ControlAction.CLOSE));
274                             return;
275                     }
276                 }
277                 return;
278             case AM43BindingConstants.CHANNEL_ID_SPEED:
279                 if (command instanceof DecimalType) {
280                     MotorSettings settings = motorSettings;
281                     if (settings != null) {
282                         DecimalType speedType = (DecimalType) command;
283                         settings.setSpeed(speedType.intValue());
284                         submitCommand(new SetSettingsCommand(settings));
285                     } else {
286                         logger.warn("Cannot set Speed before setting have been received");
287                     }
288                 }
289                 return;
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));
296                     } else {
297                         logger.warn("Cannot set Direction before setting have been received");
298                     }
299                 }
300                 return;
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));
307                     } else {
308                         logger.warn("Cannot set OperationMode before setting have been received");
309                     }
310                 }
311                 return;
312         }
313
314         super.handleCommand(channelUID, command);
315     }
316
317     @Override
318     public void receivedResponse(GetLightLevelCommand command) {
319         updateLightLevel(command.getLightLevel());
320     }
321
322     @Override
323     public void receivedResponse(GetPositionCommand command) {
324         updatePosition(command.getPosition());
325     }
326
327     @Override
328     public void receivedResponse(GetSpeedCommand command) {
329         getMotorSettings().setSpeed(command.getSpeed());
330         updateSpeed(command.getSpeed());
331     }
332
333     @Override
334     public void receivedResponse(GetAllCommand command) {
335         motorSettings = new MotorSettings();
336
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());
347     }
348
349     @Override
350     public void receivedResponse(GetBatteryLevelCommand command) {
351         updateBatteryLevel(command.getBatteryLevel());
352     }
353
354     private void updateDirection(Direction direction) {
355         getMotorSettings().setDirection(direction);
356
357         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIRECTION, new StringType(direction.toString()));
358     }
359
360     private void updateOperationMode(OperationMode opMode) {
361         getMotorSettings().setOperationMode(opMode);
362
363         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(opMode.toString()));
364     }
365
366     private void updateTopLimitSet(boolean bitValue) {
367         getMotorSettings().setTopLimitSet(bitValue);
368
369         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TOP_LIMIT_SET, OnOffType.from(bitValue));
370     }
371
372     private void updateBottomLimitSet(boolean bitValue) {
373         getMotorSettings().setBottomLimitSet(bitValue);
374
375         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_BOTTOM_LIMIT_SET, OnOffType.from(bitValue));
376     }
377
378     private void updateHasLightSensor(boolean bitValue) {
379         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_HAS_LIGHT_SENSOR, OnOffType.from(bitValue));
380     }
381
382     private void updateSpeed(int value) {
383         getMotorSettings().setSpeed(value);
384
385         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_SPEED, new DecimalType(value));
386     }
387
388     private void updatePosition(int value) {
389         if (value >= 0 && value <= 100) {
390             int percentValue = value;
391             if (getAM43Config().invertPosition) {
392                 percentValue = 100 - percentValue;
393             }
394             updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, new PercentType(percentValue));
395         } else {
396             updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, UnDefType.UNDEF);
397         }
398     }
399
400     private void updateLength(int value) {
401         getMotorSettings().setLength(value);
402
403         QuantityType<Length> lengthType = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
404         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LENGTH, lengthType);
405     }
406
407     private void updateDiameter(int value) {
408         getMotorSettings().setDiameter(value);
409
410         QuantityType<Length> diameter = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
411         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIAMETER, diameter);
412     }
413
414     private void updateType(int value) {
415         getMotorSettings().setType(value);
416
417         DecimalType type = new DecimalType(value);
418         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TYPE, type);
419     }
420
421     private void updateLightLevel(int value) {
422         DecimalType lightLevel = new DecimalType(value);
423         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL, lightLevel);
424     }
425
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);
430         } else {
431             updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_ELECTRIC, UnDefType.UNDEF);
432         }
433     }
434
435     private void updateStateIfLinked(String channelUID, State state) {
436         if (isLinked(channelUID)) {
437             updateState(channelUID, state);
438         }
439     }
440 }