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.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;
67 * The {@link LaMetricTimeHandler} is responsible for handling commands, which are
68 * sent to one of the channels.
70 * @author Gregory Moyer - Initial contribution
71 * @author Kai Kreuzer - Improved status handling, introduced refresh job and app state update
74 public class LaMetricTimeHandler extends ConfigStatusBridgeHandler {
76 private static final long CONNECTION_CHECK_INTERVAL = 60;
78 private final Logger logger = LoggerFactory.getLogger(LaMetricTimeHandler.class);
80 private final StateDescriptionOptionsProvider stateDescriptionProvider;
82 private final ClientBuilder clientBuilder;
85 private LaMetricTime clock;
88 private ScheduledFuture<?> connectionJob;
90 public LaMetricTimeHandler(Bridge bridge, StateDescriptionOptionsProvider stateDescriptionProvider,
91 ClientBuilder clientBuilder) {
93 this.clientBuilder = clientBuilder;
94 this.stateDescriptionProvider = stateDescriptionProvider;
98 public void initialize() {
99 logger.debug("Reading LaMetric Time binding configuration");
100 LaMetricTimeConfiguration bindingConfig = getConfigAs(LaMetricTimeConfiguration.class);
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);
107 connectionJob = scheduler.scheduleWithFixedDelay(() -> {
108 logger.debug("Verifying communication with LaMetric Time");
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");
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");
128 logger.debug("Setting LaMetric Time online");
129 updateStatus(ThingStatus.ONLINE);
130 }, 0, CONNECTION_CHECK_INTERVAL, TimeUnit.SECONDS);
131 updateStatus(ThingStatus.UNKNOWN);
135 public void dispose() {
136 if (connectionJob != null && !connectionJob.isCancelled()) {
137 connectionJob.cancel(true);
139 connectionJob = null;
144 public void handleCommand(ChannelUID channelUID, Command command) {
145 logger.debug("Received channel: {}, command: {}", channelUID, command);
148 switch (channelUID.getId()) {
149 case CHANNEL_NOTIFICATIONS_INFO:
150 case CHANNEL_NOTIFICATIONS_ALERT:
151 case CHANNEL_NOTIFICATIONS_WARN:
152 handleNotificationsCommand(channelUID, command);
154 case CHANNEL_DISPLAY_BRIGHTNESS:
155 case CHANNEL_DISPLAY_BRIGHTNESS_MODE:
156 handleBrightnessChannel(channelUID, command);
158 case CHANNEL_BLUETOOTH_ACTIVE:
159 handleBluetoothCommand(channelUID, command);
161 case CHANNEL_AUDIO_VOLUME:
162 handleAudioCommand(channelUID, command);
165 handleAppCommand(channelUID, command);
167 logger.debug("Channel '{}' not supported", channelUID);
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());
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.
183 * @param widgetId The current widgetId of the active app
185 public void updateActiveApp(String widgetId) {
186 updateState(LaMetricTimeBindingConstants.CHANNEL_APP, new StringType(widgetId));
189 private void handleNotificationsCommand(ChannelUID channelUID, Command command)
190 throws NotificationCreationException {
191 if (command instanceof RefreshType) {
192 // verify communication
193 clock.getLocalApi().getApi();
197 switch (channelUID.getId()) {
198 case CHANNEL_NOTIFICATIONS_INFO:
199 clock.notifyInfo(command.toString());
201 case CHANNEL_NOTIFICATIONS_WARN:
202 clock.notifyWarning(command.toString());
204 case CHANNEL_NOTIFICATIONS_ALERT:
205 clock.notifyCritical(command.toString());
208 logger.debug("Invalid notification channel: {}", channelUID);
210 updateStatus(ThingStatus.ONLINE);
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) {
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 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);
266 updateStatus(ThingStatus.ONLINE);
268 } catch (UpdateException e) {
269 logger.debug("Failed to update bluetooth - taking clock offline", e);
270 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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);
280 updateState(channelUID, OnOffType.OFF);
286 private void handleBrightnessChannel(ChannelUID channelUID, Command command) {
287 if (command instanceof RefreshType) {
288 readDisplayValue(channelUID, clock.getLocalApi().getDisplay());
290 updateDisplayValue(channelUID, command);
294 private void updateDisplayValue(ChannelUID channelUID, Command command) {
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);
304 logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
306 } else if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS_MODE)) {
307 if (command instanceof StringType) {
308 BrightnessMode mode = BrightnessMode.toEnum(command.toFullString());
310 logger.warn("Unknown brightness mode: {}", command);
312 clock.setBrightnessMode(mode);
313 updateStatus(ThingStatus.ONLINE);
316 logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
319 } catch (UpdateException e) {
320 logger.debug("Failed to update display - taking clock offline", e);
321 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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);
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);
354 public Collection<ConfigStatusMessage> getConfigStatus() {
355 Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
357 LaMetricTimeConfiguration config = getConfigAs(LaMetricTimeConfiguration.class);
358 String host = config.host;
359 String apiKey = config.apiKey;
361 if (host == null || host.isEmpty()) {
362 configStatusMessages.add(ConfigStatusMessage.Builder.error(HOST)
363 .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.HOST_MISSING).withArguments(HOST).build());
366 if (apiKey == null || apiKey.isEmpty()) {
367 configStatusMessages.add(ConfigStatusMessage.Builder.error(API_KEY)
368 .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.API_KEY_MISSING).withArguments(API_KEY)
372 return configStatusMessages;
375 protected LaMetricTime getClock() {
379 public SortedMap<String, Application> getApps() {
380 return getClock().getLocalApi().getApplications();
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)));
392 stateDescriptionProvider.setStateOptions(
393 new ChannelUID(getThing().getUID(), LaMetricTimeBindingConstants.CHANNEL_APP), options);