]> git.basschouten.com Git - openhab-addons.git/blob
e116ff6909701537669571ae174bcb0b8c078ef7
[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 percentTypeCommand) {
218             try {
219                 int volume = percentTypeCommand.intValue();
220                 if (volume >= 0 && volume != audio.getVolume()) {
221                     audio.setVolume(volume);
222                     clock.getLocalApi().updateAudio(audio);
223                     updateStatus(ThingStatus.ONLINE);
224                 }
225             } catch (UpdateException e) {
226                 logger.debug("Failed to update audio volume - taking clock offline", e);
227                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
228             }
229         }
230     }
231
232     private void handleAppCommand(ChannelUID channelUID, Command command) {
233         if (command instanceof RefreshType) {
234             logger.debug("Skipping app channel refresh - LaMetric Time does not support querying for the active app");
235         } else if (command instanceof StringType) {
236             try {
237                 WidgetRef widgetRef = WidgetRef.fromString(command.toFullString());
238                 clock.getLocalApi().activateApplication(widgetRef.getPackageName(), widgetRef.getWidgetId());
239                 updateStatus(ThingStatus.ONLINE);
240             } catch (ApplicationActivationException e) {
241                 logger.debug("Failed to activate app - taking clock offline", e);
242                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
243             }
244         }
245     }
246
247     private void handleBluetoothCommand(ChannelUID channelUID, Command command) {
248         Bluetooth bluetooth = clock.getLocalApi().getBluetooth();
249         if (command instanceof RefreshType) {
250             readBluetoothValue(channelUID, bluetooth);
251         } else {
252             updateBluetoothValue(channelUID, command, bluetooth);
253         }
254     }
255
256     private void updateBluetoothValue(ChannelUID channelUID, Command command, Bluetooth bluetooth) {
257         try {
258             if (command instanceof OnOffType onOffCommand && channelUID.getId().equals(CHANNEL_BLUETOOTH_ACTIVE)) {
259                 if (onOffCommand == OnOffType.ON && !bluetooth.isActive()) {
260                     bluetooth.setActive(true);
261                     clock.getLocalApi().updateBluetooth(bluetooth);
262                 } else if (bluetooth.isActive()) {
263                     bluetooth.setActive(false);
264                     clock.getLocalApi().updateBluetooth(bluetooth);
265                 }
266                 updateStatus(ThingStatus.ONLINE);
267             }
268         } catch (UpdateException e) {
269             logger.debug("Failed to update bluetooth - taking clock offline", e);
270             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
271         }
272     }
273
274     private void readBluetoothValue(ChannelUID channelUID, Bluetooth bluetooth) {
275         switch (channelUID.getId()) {
276             case CHANNEL_BLUETOOTH_ACTIVE:
277                 if (bluetooth.isActive()) {
278                     updateState(channelUID, OnOffType.ON);
279                 } else {
280                     updateState(channelUID, OnOffType.OFF);
281                 }
282                 break;
283         }
284     }
285
286     private void handleBrightnessChannel(ChannelUID channelUID, Command command) {
287         if (command instanceof RefreshType) {
288             readDisplayValue(channelUID, clock.getLocalApi().getDisplay());
289         } else {
290             updateDisplayValue(channelUID, command);
291         }
292     }
293
294     private void updateDisplayValue(ChannelUID channelUID, Command command) {
295         try {
296             if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS)) {
297                 if (command instanceof PercentType percentCommand) {
298                     int brightness = percentCommand.intValue();
299                     logger.debug("Set Brightness to {}.", brightness);
300                     Display newDisplay = clock.setBrightness(brightness);
301                     updateState(CHANNEL_DISPLAY_BRIGHTNESS_MODE, new StringType(newDisplay.getBrightnessMode()));
302                     updateStatus(ThingStatus.ONLINE);
303                 } else {
304                     logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
305                 }
306             } else if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS_MODE)) {
307                 if (command instanceof StringType) {
308                     BrightnessMode mode = BrightnessMode.toEnum(command.toFullString());
309                     if (mode == null) {
310                         logger.warn("Unknown brightness mode: {}", command);
311                     } else {
312                         clock.setBrightnessMode(mode);
313                         updateStatus(ThingStatus.ONLINE);
314                     }
315                 } else {
316                     logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
317                 }
318             }
319         } catch (UpdateException e) {
320             logger.debug("Failed to update display - taking clock offline", e);
321             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
322         }
323     }
324
325     private void readDisplayValue(ChannelUID channelUID, Display display) {
326         if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS)) {
327             int brightness = display.getBrightness();
328             State state = new PercentType(brightness);
329             updateState(channelUID, state);
330         } else if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS_MODE)) {
331             String mode = display.getBrightnessMode();
332             StringType state = new StringType(mode);
333             updateState(channelUID, state);
334         }
335     }
336
337     private void updateProperties(Device device, Bluetooth bluetooth) {
338         Map<String, String> properties = editProperties();
339         properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.getSerialNumber());
340         properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.getOsVersion());
341         properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
342         properties.put(LaMetricTimeBindingConstants.PROPERTY_ID, device.getId());
343         properties.put(LaMetricTimeBindingConstants.PROPERTY_NAME, device.getName());
344         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_DISCOVERABLE,
345                 String.valueOf(bluetooth.isDiscoverable()));
346         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_AVAILABLE, String.valueOf(bluetooth.isAvailable()));
347         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_PAIRABLE, String.valueOf(bluetooth.isPairable()));
348         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_MAC, bluetooth.getMac());
349         properties.put(LaMetricTimeBindingConstants.PROPERTY_BT_NAME, bluetooth.getName());
350         updateProperties(properties);
351     }
352
353     @Override
354     public Collection<ConfigStatusMessage> getConfigStatus() {
355         Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
356
357         LaMetricTimeConfiguration config = getConfigAs(LaMetricTimeConfiguration.class);
358         String host = config.host;
359         String apiKey = config.apiKey;
360
361         if (host == null || host.isEmpty()) {
362             configStatusMessages.add(ConfigStatusMessage.Builder.error(HOST)
363                     .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.HOST_MISSING).withArguments(HOST).build());
364         }
365
366         if (apiKey == null || apiKey.isEmpty()) {
367             configStatusMessages.add(ConfigStatusMessage.Builder.error(API_KEY)
368                     .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.API_KEY_MISSING).withArguments(API_KEY)
369                     .build());
370         }
371
372         return configStatusMessages;
373     }
374
375     protected LaMetricTime getClock() {
376         return clock;
377     }
378
379     public SortedMap<String, Application> getApps() {
380         return getClock().getLocalApi().getApplications();
381     }
382
383     private void setAppChannelStateDescription() {
384         List<StateOption> options = new ArrayList<>();
385         for (Application app : getApps().values()) {
386             for (Widget widget : app.getWidgets().values()) {
387                 options.add(new StateOption(new WidgetRef(widget.getPackageName(), widget.getId()).toString(),
388                         LaMetricTimeUtil.getAppLabel(app, widget)));
389             }
390         }
391
392         stateDescriptionProvider.setStateOptions(
393                 new ChannelUID(getThing().getUID(), LaMetricTimeBindingConstants.CHANNEL_APP), options);
394     }
395 }