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) {
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);
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());
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) {
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());
248 private void handleBluetoothCommand(ChannelUID channelUID, Command command) {
249 Bluetooth bluetooth = clock.getLocalApi().getBluetooth();
250 if (command instanceof RefreshType) {
251 readBluetoothValue(channelUID, bluetooth);
253 updateBluetoothValue(channelUID, command, bluetooth);
257 private void updateBluetoothValue(ChannelUID channelUID, Command command, Bluetooth bluetooth) {
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);
268 updateStatus(ThingStatus.ONLINE);
270 } catch (UpdateException e) {
271 logger.debug("Failed to update bluetooth - taking clock offline", e);
272 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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);
282 updateState(channelUID, OnOffType.OFF);
288 private void handleBrightnessChannel(ChannelUID channelUID, Command command) {
289 if (command instanceof RefreshType) {
290 readDisplayValue(channelUID, clock.getLocalApi().getDisplay());
292 updateDisplayValue(channelUID, command);
296 private void updateDisplayValue(ChannelUID channelUID, Command command) {
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);
306 logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
308 } else if (channelUID.getId().equals(CHANNEL_DISPLAY_BRIGHTNESS_MODE)) {
309 if (command instanceof StringType) {
310 BrightnessMode mode = BrightnessMode.toEnum(command.toFullString());
312 logger.warn("Unknown brightness mode: {}", command);
314 clock.setBrightnessMode(mode);
315 updateStatus(ThingStatus.ONLINE);
318 logger.debug("Unsupported command {} for display brightness! Supported commands: REFRESH", command);
321 } catch (UpdateException e) {
322 logger.debug("Failed to update display - taking clock offline", e);
323 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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);
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);
356 public Collection<ConfigStatusMessage> getConfigStatus() {
357 Collection<ConfigStatusMessage> configStatusMessages = new ArrayList<>();
359 LaMetricTimeConfiguration config = getConfigAs(LaMetricTimeConfiguration.class);
360 String host = config.host;
361 String apiKey = config.apiKey;
363 if (host == null || host.isEmpty()) {
364 configStatusMessages.add(ConfigStatusMessage.Builder.error(HOST)
365 .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.HOST_MISSING).withArguments(HOST).build());
368 if (apiKey == null || apiKey.isEmpty()) {
369 configStatusMessages.add(ConfigStatusMessage.Builder.error(API_KEY)
370 .withMessageKeySuffix(LaMetricTimeConfigStatusMessage.API_KEY_MISSING).withArguments(API_KEY)
374 return configStatusMessages;
377 protected LaMetricTime getClock() {
381 public SortedMap<String, Application> getApps() {
382 return getClock().getLocalApi().getApplications();
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)));
394 stateDescriptionProvider.setStateOptions(
395 new ChannelUID(getThing().getUID(), LaMetricTimeBindingConstants.CHANNEL_APP), options);