]> git.basschouten.com Git - openhab-addons.git/blob
2588dd0620eac29f4363a942495e0a6f306ca48e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.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;
21
22 import javax.measure.quantity.Length;
23
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;
63
64 /**
65  * The {@link AM43Handler} is responsible for handling commands, which are
66  * sent to one of the channels.
67  *
68  * @author Connor Petty - Initial contribution
69  */
70 @NonNullByDefault
71 public class AM43Handler extends ConnectedBluetoothHandler implements ResponseListener {
72
73     private final Logger logger = LoggerFactory.getLogger(AM43Handler.class);
74
75     private @Nullable AM43Configuration config;
76
77     private volatile @Nullable AM43Command currentCommand = null;
78
79     private @Nullable ScheduledFuture<?> refreshJob;
80
81     private @Nullable ExecutorService commandExecutor;
82
83     private @Nullable MotorSettings motorSettings = null;
84
85     public AM43Handler(Thing thing) {
86         super(thing);
87     }
88
89     @Override
90     public void initialize() {
91         super.initialize();
92         config = getConfigAs(AM43Configuration.class);
93
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);
100     }
101
102     @Override
103     public void dispose() {
104         dispose(commandExecutor);
105         dispose(currentCommand);
106         dispose(refreshJob);
107
108         commandExecutor = null;
109         currentCommand = null;
110         refreshJob = null;
111         motorSettings = null;
112         super.dispose();
113     }
114
115     private static void dispose(@Nullable ExecutorService executor) {
116         if (executor != null) {
117             executor.shutdownNow();
118         }
119     }
120
121     private static void dispose(@Nullable ScheduledFuture<?> future) {
122         if (future != null) {
123             future.cancel(true);
124         }
125     }
126
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
131             // terminate
132             command.setState(AM43Command.State.FAILED);
133         }
134     }
135
136     private MotorSettings getMotorSettings() {
137         MotorSettings settings = motorSettings;
138         if (settings == null) {
139             throw new IllegalStateException("motorSettings has not been initialized");
140         }
141         return settings;
142     }
143
144     private AM43Configuration getAM43Config() {
145         AM43Configuration ret = config;
146         if (ret == null) {
147             throw new IllegalStateException("config has not been initialized");
148         }
149         return ret;
150     }
151
152     private void submitCommand(AM43Command command) {
153         Executor executor = commandExecutor;
154         if (executor != null) {
155             executor.execute(() -> processCommand(command));
156         }
157     }
158
159     private void processCommand(AM43Command command) {
160         try {
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);
165                 return;
166             }
167             if (!resolved) {
168                 logger.debug("Unable to send command {} to device {}: services not resolved", command,
169                         device.getAddress());
170                 command.setState(AM43Command.State.FAILED);
171                 return;
172             }
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);
178                 return;
179             }
180             // there is no consequence to calling this as much as we like
181             device.enableNotifications(characteristic);
182
183             characteristic.setValue(command.getRequest());
184             command.setState(AM43Command.State.ENQUEUED);
185             device.writeCharacteristic(characteristic);
186
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());
190             }
191         } catch (InterruptedException e) {
192             // do nothing
193         } finally {
194             logger.trace("Command final state: {}", command.getState());
195             currentCommand = null;
196         }
197     }
198
199     @Override
200     public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
201             BluetoothCompletionStatus status) {
202         super.onCharacteristicWriteComplete(characteristic, status);
203
204         byte[] request = characteristic.getByteValue();
205
206         AM43Command command = currentCommand;
207
208         if (command != null) {
209             if (!Arrays.equals(request, command.getRequest())) {
210                 logger.debug("Write completed for unknown command");
211                 return;
212             }
213             switch (status) {
214                 case SUCCESS:
215                     command.setState(AM43Command.State.SENT);
216                     break;
217                 case ERROR:
218                     command.setState(AM43Command.State.FAILED);
219                     break;
220             }
221         } else {
222             if (logger.isDebugEnabled()) {
223                 logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
224             }
225         }
226     }
227
228     @Override
229     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
230         super.onCharacteristicUpdate(characteristic);
231
232         byte[] response = characteristic.getByteValue();
233
234         AM43Command command = currentCommand;
235         if (command == null) {
236             if (logger.isDebugEnabled()) {
237                 logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response));
238             }
239         } else if (!command.handleResponse(scheduler, this, response)) {
240             if (logger.isDebugEnabled()) {
241                 logger.debug("Command {} could not handle response {}", command, HexUtils.bytesToHex(response));
242             }
243         }
244     }
245
246     @Override
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());
252                     return;
253                 case AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL:
254                     submitCommand(new GetLightLevelCommand());
255                     return;
256                 case AM43BindingConstants.CHANNEL_ID_POSITION:
257                     submitCommand(new GetPositionCommand());
258                     return;
259             }
260             submitCommand(new GetAllCommand());
261             return;
262         }
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.");
269                         return;
270                     }
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.");
274                         return;
275                     }
276                     PercentType percent = (PercentType) command;
277                     int value = percent.intValue();
278                     if (getAM43Config().invertPosition) {
279                         value = 100 - value;
280                     }
281                     submitCommand(new SetPositionCommand(value));
282                     return;
283                 }
284                 if (command instanceof StopMoveType) {
285                     switch ((StopMoveType) command) {
286                         case STOP:
287                             submitCommand(new ControlCommand(ControlAction.STOP));
288                             return;
289                         case MOVE:
290                             // do nothing
291                             return;
292                     }
293                 }
294                 if (command instanceof UpDownType) {
295                     switch ((UpDownType) command) {
296                         case UP:
297                             submitCommand(new ControlCommand(ControlAction.OPEN));
298                             return;
299                         case DOWN:
300                             submitCommand(new ControlCommand(ControlAction.CLOSE));
301                             return;
302                     }
303                 }
304                 return;
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));
312                     } else {
313                         logger.warn("Cannot set Speed before setting have been received");
314                     }
315                 }
316                 return;
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));
323                     } else {
324                         logger.warn("Cannot set Direction before setting have been received");
325                     }
326                 }
327                 return;
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));
334                     } else {
335                         logger.warn("Cannot set OperationMode before setting have been received");
336                     }
337                 }
338                 return;
339         }
340
341         super.handleCommand(channelUID, command);
342     }
343
344     @Override
345     public void receivedResponse(GetLightLevelCommand command) {
346         updateLightLevel(command.getLightLevel());
347     }
348
349     @Override
350     public void receivedResponse(GetPositionCommand command) {
351         updatePosition(command.getPosition());
352     }
353
354     @Override
355     public void receivedResponse(GetSpeedCommand command) {
356         getMotorSettings().setSpeed(command.getSpeed());
357         updateSpeed(command.getSpeed());
358     }
359
360     @Override
361     public void receivedResponse(GetAllCommand command) {
362         motorSettings = new MotorSettings();
363
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());
374     }
375
376     @Override
377     public void receivedResponse(GetBatteryLevelCommand command) {
378         updateBatteryLevel(command.getBatteryLevel());
379     }
380
381     private void updateDirection(Direction direction) {
382         getMotorSettings().setDirection(direction);
383
384         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIRECTION, new StringType(direction.toString()));
385     }
386
387     private void updateOperationMode(OperationMode opMode) {
388         getMotorSettings().setOperationMode(opMode);
389
390         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(opMode.toString()));
391     }
392
393     private void updateTopLimitSet(boolean bitValue) {
394         getMotorSettings().setTopLimitSet(bitValue);
395
396         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TOP_LIMIT_SET, OnOffType.from(bitValue));
397     }
398
399     private void updateBottomLimitSet(boolean bitValue) {
400         getMotorSettings().setBottomLimitSet(bitValue);
401
402         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_BOTTOM_LIMIT_SET, OnOffType.from(bitValue));
403     }
404
405     private void updateHasLightSensor(boolean bitValue) {
406         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_HAS_LIGHT_SENSOR, OnOffType.from(bitValue));
407     }
408
409     private void updateSpeed(int value) {
410         getMotorSettings().setSpeed(value);
411
412         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_SPEED, new DecimalType(value));
413     }
414
415     private void updatePosition(int value) {
416         if (value >= 0 && value <= 100) {
417             int percentValue = value;
418             if (getAM43Config().invertPosition) {
419                 percentValue = 100 - percentValue;
420             }
421             updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, new PercentType(percentValue));
422         } else {
423             updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_POSITION, UnDefType.UNDEF);
424         }
425     }
426
427     private void updateLength(int value) {
428         getMotorSettings().setLength(value);
429
430         QuantityType<Length> lengthType = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
431         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LENGTH, lengthType);
432     }
433
434     private void updateDiameter(int value) {
435         getMotorSettings().setDiameter(value);
436
437         QuantityType<Length> diameter = QuantityType.valueOf(value, MetricPrefix.MILLI(SIUnits.METRE));
438         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_DIAMETER, diameter);
439     }
440
441     private void updateType(int value) {
442         getMotorSettings().setType(value);
443
444         DecimalType type = new DecimalType(value);
445         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_TYPE, type);
446     }
447
448     private void updateLightLevel(int value) {
449         DecimalType lightLevel = new DecimalType(value);
450         updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_LIGHT_LEVEL, lightLevel);
451     }
452
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);
457         } else {
458             updateStateIfLinked(AM43BindingConstants.CHANNEL_ID_ELECTRIC, UnDefType.UNDEF);
459         }
460     }
461
462     private void updateStateIfLinked(String channelUID, State state) {
463         if (isLinked(channelUID)) {
464             updateState(channelUID, state);
465         }
466     }
467 }