2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lametrictime.internal.handler;
15 import static org.openhab.binding.lametrictime.internal.LaMetricTimeBindingConstants.*;
16 import static org.openhab.binding.lametrictime.internal.config.LaMetricTimeConfiguration.*;
18 import java.util.ArrayList;
19 import java.util.Collection;
20 import java.util.List;
22 import java.util.SortedMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import javax.ws.rs.client.ClientBuilder;
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;
65 * The {@link LaMetricTimeHandler} is responsible for handling commands, which are
66 * sent to one of the channels.
68 * @author Gregory Moyer - Initial contribution
69 * @author Kai Kreuzer - Improved status handling, introduced refresh job and app state update
71 public class LaMetricTimeHandler extends ConfigStatusBridgeHandler {
73 private static final long CONNECTION_CHECK_INTERVAL = 60;
75 private final Logger logger = LoggerFactory.getLogger(LaMetricTimeHandler.class);
77 private final StateDescriptionOptionsProvider stateDescriptionProvider;
79 private final ClientBuilder clientBuilder;
81 private LaMetricTime clock;
83 private ScheduledFuture<?> connectionJob;
85 public LaMetricTimeHandler(Bridge bridge, StateDescriptionOptionsProvider stateDescriptionProvider,
86 ClientBuilder clientBuilder) {
88 this.clientBuilder = clientBuilder;
89 this.stateDescriptionProvider = stateDescriptionProvider;
91 if (stateDescriptionProvider == null) {
92 logger.warn("State description provider is null");
97 public void initialize() {
98 logger.debug("Reading LaMetric Time binding configuration");
99 LaMetricTimeConfiguration bindingConfig = getConfigAs(LaMetricTimeConfiguration.class);
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);
106 connectionJob = scheduler.scheduleWithFixedDelay(() -> {
107 logger.debug("Verifying communication with LaMetric Time");
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");
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");
127 logger.debug("Setting LaMetric Time online");
128 updateStatus(ThingStatus.ONLINE);
129 }, 0, CONNECTION_CHECK_INTERVAL, TimeUnit.SECONDS);
130 updateStatus(ThingStatus.UNKNOWN);
134 public void dispose() {
135 if (connectionJob != null && !connectionJob.isCancelled()) {
136 connectionJob.cancel(true);
138 connectionJob = null;
143 public void handleCommand(ChannelUID channelUID, Command command) {
144 logger.debug("Received channel: {}, command: {}", channelUID, command);
147 switch (channelUID.getId()) {
148 case CHANNEL_NOTIFICATIONS_INFO:
149 case CHANNEL_NOTIFICATIONS_ALERT:
150 case CHANNEL_NOTIFICATIONS_WARN:
151 handleNotificationsCommand(channelUID, command);
153 case CHANNEL_DISPLAY_BRIGHTNESS:
154 case CHANNEL_DISPLAY_BRIGHTNESS_MODE:
155 handleBrightnessChannel(channelUID, command);
157 case CHANNEL_BLUETOOTH_ACTIVE:
158 handleBluetoothCommand(channelUID, command);
160 case CHANNEL_AUDIO_VOLUME:
161 handleAudioCommand(channelUID, command);
164 handleAppCommand(channelUID, command);
166 logger.debug("Channel '{}' not supported", channelUID);
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());
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.
182 * @param widgetId The current widgetId of the active app
184 public void updateActiveApp(String widgetId) {
185 updateState(LaMetricTimeBindingConstants.CHANNEL_APP, new StringType(widgetId));
188 private void handleNotificationsCommand(ChannelUID channelUID, Command command)
189 throws NotificationCreationException {
190 if (command instanceof RefreshType) {
191 // verify communication
192 clock.getLocalApi().getApi();
196 switch (channelUID.getId()) {
197 case CHANNEL_NOTIFICATIONS_INFO:
198 clock.notifyInfo(command.toString());
200 case CHANNEL_NOTIFICATIONS_WARN:
201 clock.notifyWarning(command.toString());
203 case CHANNEL_NOTIFICATIONS_ALERT:
204 clock.notifyCritical(command.toString());
207 logger.debug("Invalid notification channel: {}", channelUID);
209 updateStatus(ThingStatus.ONLINE);
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) {
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);
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());
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) {
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());
247 private void handleBluetoothCommand(ChannelUID channelUID, Command command) {
248 Bluetooth bluetooth = clock.getLocalApi().getBluetooth();
249 if (command instanceof RefreshType) {
250 readBluetoothValue(channelUID, bluetooth);
252 updateBluetoothValue(channelUID, command, bluetooth);
256 private void updateBluetoothValue(ChannelUID channelUID, Command command, Bluetooth bluetooth) {
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);
267 updateStatus(ThingStatus.ONLINE);
269 } catch (UpdateException e) {
270 logger.debug("Failed to update bluetooth - taking clock offline", e);
271 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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);
281 updateState(channelUID, OnOffType.OFF);
287 private void handleBrightnessChannel(ChannelUID channelUID, Command command) {
288 if (command instanceof RefreshType) {
289 readDisplayValue(channelUID, clock.getLocalApi().getDisplay());
291 updateDisplayValue(channelUID, command);
295 private void updateDisplayValue(ChannelUID channelUID, Command command) {
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);
305 logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
307 } else if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS_MODE)) {
308 if (command instanceof StringType) {
309 BrightnessMode mode = BrightnessMode.toEnum(command.toFullString());
311 logger.warn("Unknown brightness mode: {}", command);
313 clock.setBrightnessMode(mode);
314 updateStatus(ThingStatus.ONLINE);
317 logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
320 } catch (UpdateException e) {
321 logger.debug("Failed to update display - taking clock offline", e);
322 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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);
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);
355 public Collection<ConfigStatusMessage> getConfigStatus() {
356 Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
358 LaMetricTimeConfiguration config = getConfigAs(LaMetricTimeConfiguration.class);
359 String host = config.host;
360 String apiKey = config.apiKey;
362 if (host == null || host.isEmpty()) {
363 configStatusMessages.add(ConfigStatusMessage.Builder.error(HOST)
364 .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.HOST_MISSING).withArguments(HOST).build());
367 if (apiKey == null || apiKey.isEmpty()) {
368 configStatusMessages.add(ConfigStatusMessage.Builder.error(API_KEY)
369 .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.API_KEY_MISSING).withArguments(API_KEY)
373 return configStatusMessages;
376 protected LaMetricTime getClock() {
380 public SortedMap<String, Application> getApps() {
381 return getClock().getLocalApi().getApplications();
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)));
393 stateDescriptionProvider.setStateOptions(
394 new ChannelUID(getThing().getUID(), LaMetricTimeBindingConstants.CHANNEL_APP), options);