]> git.basschouten.com Git - openhab-addons.git/blob
26690b6529a37c2ec25997acd43dcc297c5b1971
[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.lametrictime.internal.handler;
14
15 import static org.openhab.binding.lametrictime.internal.LaMetricTimeBindingConstants.*;
16 import static org.openhab.binding.lametrictime.internal.config.LaMetricTimeConfiguration.*;
17
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.SortedMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.ws.rs.client.ClientBuilder;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.lametrictime.internal.LaMetricTimeBindingConstants;
31 import org.openhab.binding.lametrictime.internal.LaMetricTimeConfigStatusMessage;
32 import org.openhab.binding.lametrictime.internal.LaMetricTimeUtil;
33 import org.openhab.binding.lametrictime.internal.StateDescriptionOptionsProvider;
34 import org.openhab.binding.lametrictime.internal.WidgetRef;
35 import org.openhab.binding.lametrictime.internal.api.Configuration;
36 import org.openhab.binding.lametrictime.internal.api.LaMetricTime;
37 import org.openhab.binding.lametrictime.internal.api.dto.enums.BrightnessMode;
38 import org.openhab.binding.lametrictime.internal.api.local.ApplicationActivationException;
39 import org.openhab.binding.lametrictime.internal.api.local.LaMetricTimeLocal;
40 import org.openhab.binding.lametrictime.internal.api.local.NotificationCreationException;
41 import org.openhab.binding.lametrictime.internal.api.local.UpdateException;
42 import org.openhab.binding.lametrictime.internal.api.local.dto.Application;
43 import org.openhab.binding.lametrictime.internal.api.local.dto.Audio;
44 import org.openhab.binding.lametrictime.internal.api.local.dto.Bluetooth;
45 import org.openhab.binding.lametrictime.internal.api.local.dto.Device;
46 import org.openhab.binding.lametrictime.internal.api.local.dto.Display;
47 import org.openhab.binding.lametrictime.internal.api.local.dto.Widget;
48 import org.openhab.binding.lametrictime.internal.config.LaMetricTimeConfiguration;
49 import org.openhab.core.config.core.status.ConfigStatusMessage;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.PercentType;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.RefreshType;
61 import org.openhab.core.types.State;
62 import org.openhab.core.types.StateOption;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66 /**
67  * The {@link LaMetricTimeHandler} is responsible for handling commands, which are
68  * sent to one of the channels.
69  *
70  * @author Gregory Moyer - Initial contribution
71  * @author Kai Kreuzer - Improved status handling, introduced refresh job and app state update
72  */
73 @NonNullByDefault
74 public class LaMetricTimeHandler extends ConfigStatusBridgeHandler {
75
76     private static final long CONNECTION_CHECK_INTERVAL = 60;
77
78     private final Logger logger = LoggerFactory.getLogger(LaMetricTimeHandler.class);
79
80     private final StateDescriptionOptionsProvider stateDescriptionProvider;
81
82     private final ClientBuilder clientBuilder;
83
84     @NonNullByDefault({})
85     private LaMetricTime clock;
86
87     @Nullable
88     private ScheduledFuture<?> connectionJob;
89
90     public LaMetricTimeHandler(Bridge bridge, StateDescriptionOptionsProvider stateDescriptionProvider,
91             ClientBuilder clientBuilder) {
92         super(bridge);
93         this.clientBuilder = clientBuilder;
94         this.stateDescriptionProvider = stateDescriptionProvider;
95     }
96
97     @Override
98     public void initialize() {
99         logger.debug("Reading LaMetric Time binding configuration");
100         LaMetricTimeConfiguration bindingConfig = getConfigAs(LaMetricTimeConfiguration.class);
101
102         logger.debug("Creating LaMetric Time client");
103         Configuration clockConfig = new Configuration().withDeviceHost(bindingConfig.host)
104                 .withDeviceApiKey(bindingConfig.apiKey).withLogging(logger.isDebugEnabled());
105         clock = LaMetricTime.create(clockConfig, clientBuilder);
106
107         connectionJob = scheduler.scheduleWithFixedDelay(() -> {
108             logger.debug("Verifying communication with LaMetric Time");
109             try {
110                 LaMetricTimeLocal api = clock.getLocalApi();
111                 Device device = api.getDevice();
112                 if (device == null) {
113                     logger.debug("Failed to communicate with LaMetric Time");
114                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
115                             "Unable to connect to LaMetric Time");
116                     return;
117                 }
118
119                 updateProperties(device, api.getBluetooth());
120                 setAppChannelStateDescription();
121             } catch (Exception e) {
122                 logger.debug("Failed to communicate with LaMetric Time", e);
123                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
124                         "Unable to connect to LaMetric Time");
125                 return;
126             }
127
128             logger.debug("Setting LaMetric Time online");
129             updateStatus(ThingStatus.ONLINE);
130         }, 0, CONNECTION_CHECK_INTERVAL, TimeUnit.SECONDS);
131         updateStatus(ThingStatus.UNKNOWN);
132     }
133
134     @Override
135     public void dispose() {
136         if (connectionJob != null && !connectionJob.isCancelled()) {
137             connectionJob.cancel(true);
138         }
139         connectionJob = null;
140         clock = null;
141     }
142
143     @Override
144     public void handleCommand(ChannelUID channelUID, Command command) {
145         logger.debug("Received channel: {}, command: {}", channelUID, command);
146
147         try {
148             switch (channelUID.getId()) {
149                 case CHANNEL_NOTIFICATIONS_INFO:
150                 case CHANNEL_NOTIFICATIONS_ALERT:
151                 case CHANNEL_NOTIFICATIONS_WARN:
152                     handleNotificationsCommand(channelUID, command);
153                     break;
154                 case CHANNEL_DISPLAY_BRIGHTNESS:
155                 case CHANNEL_DISPLAY_BRIGHTNESS_MODE:
156                     handleBrightnessChannel(channelUID, command);
157                     break;
158                 case CHANNEL_BLUETOOTH_ACTIVE:
159                     handleBluetoothCommand(channelUID, command);
160                     break;
161                 case CHANNEL_AUDIO_VOLUME:
162                     handleAudioCommand(channelUID, command);
163                     break;
164                 case CHANNEL_APP:
165                     handleAppCommand(channelUID, command);
166                 default:
167                     logger.debug("Channel '{}' not supported", channelUID);
168                     break;
169             }
170         } catch (NotificationCreationException e) {
171             logger.debug("Failed to create notification - taking clock offline", e);
172             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
173         } catch (Exception e) {
174             logger.debug("Unexpected error while handling command - taking clock offline", e);
175             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
176         }
177     }
178
179     /**
180      * This method can be called by app-specific thing handlers to update the state of the "app" channel on the device.
181      * Note: When sending a command to an app, the device automatically switches to this app, so we reflect this here.
182      *
183      * @param widgetId The current widgetId of the active app
184      */
185     public void updateActiveApp(String widgetId) {
186         updateState(LaMetricTimeBindingConstants.CHANNEL_APP, new StringType(widgetId));
187     }
188
189     private void handleNotificationsCommand(ChannelUID channelUID, Command command)
190             throws NotificationCreationException {
191         if (command instanceof RefreshType) {
192             // verify communication
193             clock.getLocalApi().getApi();
194             return;
195         }
196
197         switch (channelUID.getId()) {
198             case CHANNEL_NOTIFICATIONS_INFO:
199                 clock.notifyInfo(command.toString());
200                 break;
201             case CHANNEL_NOTIFICATIONS_WARN:
202                 clock.notifyWarning(command.toString());
203                 break;
204             case CHANNEL_NOTIFICATIONS_ALERT:
205                 clock.notifyCritical(command.toString());
206                 break;
207             default:
208                 logger.debug("Invalid notification channel: {}", channelUID);
209         }
210         updateStatus(ThingStatus.ONLINE);
211     }
212
213     private void handleAudioCommand(ChannelUID channelUID, Command command) {
214         Audio audio = clock.getLocalApi().getAudio();
215         if (command instanceof RefreshType) {
216             updateState(channelUID, new PercentType(audio.getVolume()));
217         } else if (command instanceof PercentType) {
218             try {
219                 PercentType percentTypeCommand = (PercentType) command;
220                 int volume = percentTypeCommand.intValue();
221                 if (volume >= 0 && volume != audio.getVolume()) {
222                     audio.setVolume(volume);
223                     clock.getLocalApi().updateAudio(audio);
224                     updateStatus(ThingStatus.ONLINE);
225                 }
226             } catch (UpdateException e) {
227                 logger.debug("Failed to update audio volume - taking clock offline", e);
228                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
229             }
230         }
231     }
232
233     private void handleAppCommand(ChannelUID channelUID, Command command) {
234         if (command instanceof RefreshType) {
235             logger.debug("Skipping app channel refresh - LaMetric Time does not support querying for the active app");
236         } else if (command instanceof StringType) {
237             try {
238                 WidgetRef widgetRef = WidgetRef.fromString(command.toFullString());
239                 clock.getLocalApi().activateApplication(widgetRef.getPackageName(), widgetRef.getWidgetId());
240                 updateStatus(ThingStatus.ONLINE);
241             } catch (ApplicationActivationException e) {
242                 logger.debug("Failed to activate app - taking clock offline", e);
243                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
244             }
245         }
246     }
247
248     private void handleBluetoothCommand(ChannelUID channelUID, Command command) {
249         Bluetooth bluetooth = clock.getLocalApi().getBluetooth();
250         if (command instanceof RefreshType) {
251             readBluetoothValue(channelUID, bluetooth);
252         } else {
253             updateBluetoothValue(channelUID, command, bluetooth);
254         }
255     }
256
257     private void updateBluetoothValue(ChannelUID channelUID, Command command, Bluetooth bluetooth) {
258         try {
259             if (command instanceof OnOffType && channelUID.getId().equals(CHANNEL_BLUETOOTH_ACTIVE)) {
260                 OnOffType onOffCommand = (OnOffType) command;
261                 if (onOffCommand == OnOffType.ON && !bluetooth.isActive()) {
262                     bluetooth.setActive(true);
263                     clock.getLocalApi().updateBluetooth(bluetooth);
264                 } else if (bluetooth.isActive()) {
265                     bluetooth.setActive(false);
266                     clock.getLocalApi().updateBluetooth(bluetooth);
267                 }
268                 updateStatus(ThingStatus.ONLINE);
269             }
270         } catch (UpdateException e) {
271             logger.debug("Failed to update bluetooth - taking clock offline", e);
272             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
273         }
274     }
275
276     private void readBluetoothValue(ChannelUID channelUID, Bluetooth bluetooth) {
277         switch (channelUID.getId()) {
278             case CHANNEL_BLUETOOTH_ACTIVE:
279                 if (bluetooth.isActive()) {
280                     updateState(channelUID, OnOffType.ON);
281                 } else {
282                     updateState(channelUID, OnOffType.OFF);
283                 }
284                 break;
285         }
286     }
287
288     private void handleBrightnessChannel(ChannelUID channelUID, Command command) {
289         if (command instanceof RefreshType) {
290             readDisplayValue(channelUID, clock.getLocalApi().getDisplay());
291         } else {
292             updateDisplayValue(channelUID, command);
293         }
294     }
295
296     private void updateDisplayValue(ChannelUID channelUID, Command command) {
297         try {
298             if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS)) {
299                 if (command instanceof PercentType) {
300                     int brightness = ((PercentType) command).intValue();
301                     logger.debug("Set Brightness to {}.", brightness);
302                     Display newDisplay = clock.setBrightness(brightness);
303                     updateState(CHANNEL_DISPLAY_BRIGHTNESS_MODE, new StringType(newDisplay.getBrightnessMode()));
304                     updateStatus(ThingStatus.ONLINE);
305                 } else {
306                     logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
307                 }
308             } else if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS_MODE)) {
309                 if (command instanceof StringType) {
310                     BrightnessMode mode = BrightnessMode.toEnum(command.toFullString());
311                     if (mode == null) {
312                         logger.warn("Unknown brightness mode: {}", command);
313                     } else {
314                         clock.setBrightnessMode(mode);
315                         updateStatus(ThingStatus.ONLINE);
316                     }
317                 } else {
318                     logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
319                 }
320             }
321         } catch (UpdateException e) {
322             logger.debug("Failed to update display - taking clock offline", e);
323             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
324         }
325     }
326
327     private void readDisplayValue(ChannelUID channelUID, Display display) {
328         if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS)) {
329             int brightness = display.getBrightness();
330             State state = new PercentType(brightness);
331             updateState(channelUID, state);
332         } else if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS_MODE)) {
333             String mode = display.getBrightnessMode();
334             StringType state = new StringType(mode);
335             updateState(channelUID, state);
336         }
337     }
338
339     private void updateProperties(Device device, Bluetooth bluetooth) {
340         Map<String, String> properties = editProperties();
341         properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber());
342         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getOsVersion());
343         properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
344         properties.put(LaMetricTimeBindingConstants.PROPERTY_ID, device.getId());
345         properties.put(LaMetricTimeBindingConstants.PROPERTY_NAME, device.getName());
346         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_DISCOVERABLE,
347                 String.valueOf(bluetooth.isDiscoverable()));
348         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_AVAILABLE, String.valueOf(bluetooth.isAvailable()));
349         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_PAIRABLE, String.valueOf(bluetooth.isPairable()));
350         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_MAC, bluetooth.getMac());
351         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_NAME, bluetooth.getName());
352         updateProperties(properties);
353     }
354
355     @Override
356     public Collection<ConfigStatusMessage> getConfigStatus() {
357         Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
358
359         LaMetricTimeConfiguration config = getConfigAs(LaMetricTimeConfiguration.class);
360         String host = config.host;
361         String apiKey = config.apiKey;
362
363         if (host == null || host.isEmpty()) {
364             configStatusMessages.add(ConfigStatusMessage.Builder.error(HOST)
365                     .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.HOST_MISSING).withArguments(HOST).build());
366         }
367
368         if (apiKey == null || apiKey.isEmpty()) {
369             configStatusMessages.add(ConfigStatusMessage.Builder.error(API_KEY)
370                     .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.API_KEY_MISSING).withArguments(API_KEY)
371                     .build());
372         }
373
374         return configStatusMessages;
375     }
376
377     protected LaMetricTime getClock() {
378         return clock;
379     }
380
381     public SortedMap<String, Application> getApps() {
382         return getClock().getLocalApi().getApplications();
383     }
384
385     private void setAppChannelStateDescription() {
386         List<StateOption> options = new ArrayList<>();
387         for (Application app : getApps().values()) {
388             for (Widget widget : app.getWidgets().values()) {
389                 options.add(new StateOption(new WidgetRef(widget.getPackageName(), widget.getId()).toString(),
390                         LaMetricTimeUtil.getAppLabel(app, widget)));
391             }
392         }
393
394         stateDescriptionProvider.setStateOptions(
395                 new ChannelUID(getThing().getUID(), LaMetricTimeBindingConstants.CHANNEL_APP), options);
396     }
397 }