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