2 * Copyright (c) 2010-2024 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.lgwebos.internal.handler;
15 import static org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants.*;
17 import java.net.InetAddress;
18 import java.net.UnknownHostException;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.websocket.client.WebSocketClient;
31 import org.openhab.binding.lgwebos.internal.ChannelHandler;
32 import org.openhab.binding.lgwebos.internal.LGWebOSBindingConstants;
33 import org.openhab.binding.lgwebos.internal.LGWebOSStateDescriptionOptionProvider;
34 import org.openhab.binding.lgwebos.internal.LauncherApplication;
35 import org.openhab.binding.lgwebos.internal.MediaControlPlayer;
36 import org.openhab.binding.lgwebos.internal.MediaControlStop;
37 import org.openhab.binding.lgwebos.internal.PowerControlPower;
38 import org.openhab.binding.lgwebos.internal.RCButtonControl;
39 import org.openhab.binding.lgwebos.internal.TVControlChannel;
40 import org.openhab.binding.lgwebos.internal.ToastControlToast;
41 import org.openhab.binding.lgwebos.internal.VolumeControlMute;
42 import org.openhab.binding.lgwebos.internal.VolumeControlVolume;
43 import org.openhab.binding.lgwebos.internal.WakeOnLanUtility;
44 import org.openhab.binding.lgwebos.internal.action.LGWebOSActions;
45 import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.WebOSTVSocketListener;
46 import org.openhab.binding.lgwebos.internal.handler.core.AppInfo;
47 import org.openhab.binding.lgwebos.internal.handler.core.ResponseListener;
48 import org.openhab.core.config.core.Configuration;
49 import org.openhab.core.library.types.OnOffType;
50 import org.openhab.core.thing.ChannelUID;
51 import org.openhab.core.thing.Thing;
52 import org.openhab.core.thing.ThingStatus;
53 import org.openhab.core.thing.ThingStatusDetail;
54 import org.openhab.core.thing.binding.BaseThingHandler;
55 import org.openhab.core.thing.binding.ThingHandlerService;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.State;
58 import org.openhab.core.types.StateOption;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
63 * The {@link LGWebOSHandler} is responsible for handling commands, which are
64 * sent to one of the channels.
66 * @author Sebastian Prehn - initial contribution
69 public class LGWebOSHandler extends BaseThingHandler
70 implements LGWebOSTVSocket.ConfigProvider, WebOSTVSocketListener, PowerControlPower.ConfigProvider {
73 * constants for device polling
75 private static final int RECONNECT_INTERVAL_SECONDS = 10;
76 private static final int RECONNECT_START_UP_DELAY_SECONDS = 0;
77 private static final int CHANNEL_SUBSCRIPTION_DELAY_SECONDS = 1;
78 private static final String APP_ID_LIVETV = "com.webos.app.livetv";
80 private final Logger logger = LoggerFactory.getLogger(LGWebOSHandler.class);
82 // ChannelID to CommandHandler Map
83 private final Map<String, ChannelHandler> channelHandlers;
85 private final LauncherApplication appLauncher = new LauncherApplication();
87 private final WebSocketClient webSocketClient;
89 private final LGWebOSStateDescriptionOptionProvider stateDescriptionProvider;
91 private @Nullable LGWebOSTVSocket socket;
93 private @Nullable ScheduledFuture<?> reconnectJob;
94 private @Nullable ScheduledFuture<?> keepAliveJob;
95 private @Nullable ScheduledFuture<?> channelSubscriptionJob;
97 private @Nullable LGWebOSConfiguration config;
99 public LGWebOSHandler(Thing thing, WebSocketClient webSocketClient,
100 LGWebOSStateDescriptionOptionProvider stateDescriptionProvider) {
102 this.webSocketClient = webSocketClient;
103 this.stateDescriptionProvider = stateDescriptionProvider;
105 Map<String, ChannelHandler> handlers = new HashMap<>();
106 handlers.put(CHANNEL_VOLUME, new VolumeControlVolume());
107 handlers.put(CHANNEL_POWER, new PowerControlPower(this, scheduler));
108 handlers.put(CHANNEL_MUTE, new VolumeControlMute());
109 handlers.put(CHANNEL_CHANNEL, new TVControlChannel());
110 handlers.put(CHANNEL_APP_LAUNCHER, appLauncher);
111 handlers.put(CHANNEL_MEDIA_STOP, new MediaControlStop());
112 handlers.put(CHANNEL_TOAST, new ToastControlToast());
113 handlers.put(CHANNEL_MEDIA_PLAYER, new MediaControlPlayer());
114 handlers.put(CHANNEL_RCBUTTON, new RCButtonControl());
115 channelHandlers = Collections.unmodifiableMap(handlers);
118 private LGWebOSConfiguration getLGWebOSConfig() {
119 LGWebOSConfiguration c = config;
121 c = getConfigAs(LGWebOSConfiguration.class);
128 public void initialize() {
129 logger.debug("Initializing handler for thing {}", getThing().getUID());
130 LGWebOSConfiguration c = getLGWebOSConfig();
131 logger.trace("Handler initialized with config {}", c);
132 String host = c.getHost();
133 if (host.isEmpty()) {
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
135 "@text/offline.config-error-unknown-host");
139 LGWebOSTVSocket s = new LGWebOSTVSocket(webSocketClient, this, host, c.getUseTLS(), scheduler);
143 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.tv-off");
149 public void dispose() {
150 logger.debug("Disposing handler for thing {}", getThing().getUID());
153 stopChannelSubscriptionJob();
155 LGWebOSTVSocket s = socket;
161 config = null; // ensure config gets actually refreshed during re-initialization
165 private void startReconnectJob() {
166 ScheduledFuture<?> job = reconnectJob;
167 if (job == null || job.isCancelled()) {
168 reconnectJob = scheduler.scheduleWithFixedDelay(() -> {
169 getSocket().disconnect();
170 getSocket().connect();
171 }, RECONNECT_START_UP_DELAY_SECONDS, RECONNECT_INTERVAL_SECONDS, TimeUnit.SECONDS);
175 private void stopReconnectJob() {
176 ScheduledFuture<?> job = reconnectJob;
177 if (job != null && !job.isCancelled()) {
184 * Keep alive ensures that the web socket connection is used and does not time out.
186 private void startKeepAliveJob() {
187 ScheduledFuture<?> job = keepAliveJob;
188 if (job == null || job.isCancelled()) {
189 // half of idle time out setting
190 long keepAliveInterval = this.webSocketClient.getMaxIdleTimeout() / 2;
192 // it is irrelevant which service is queried. Only need to send some packets over the wire
194 keepAliveJob = scheduler.scheduleWithFixedDelay(() -> getSocket().getRunningApp(new ResponseListener<>() {
197 public void onSuccess(AppInfo responseObject) {
198 // ignore - actual response is not relevant here
202 public void onError(String message) {
205 }), keepAliveInterval, keepAliveInterval, TimeUnit.MILLISECONDS);
210 private void stopKeepAliveJob() {
211 ScheduledFuture<?> job = keepAliveJob;
212 if (job != null && !job.isCancelled()) {
218 public LGWebOSTVSocket getSocket() {
219 LGWebOSTVSocket s = this.socket;
221 throw new IllegalStateException("Component called before it was initialized or already disposed.");
226 public LauncherApplication getLauncherApplication() {
231 public void handleCommand(ChannelUID channelUID, Command command) {
232 logger.debug("handleCommand({},{})", channelUID, command);
233 ChannelHandler handler = channelHandlers.get(channelUID.getId());
234 if (handler == null) {
236 "Unable to handle command {}. No handler found for channel {}. This must not happen. Please report as a bug.",
237 command, channelUID);
241 handler.onReceiveCommand(channelUID.getId(), this, command);
245 public String getMacAddress() {
246 return getLGWebOSConfig().getMacAddress();
250 public String getKey() {
251 return getLGWebOSConfig().getKey();
255 public void storeKey(@Nullable String key) {
256 if (!getKey().equals(key)) {
257 logger.debug("Store new access Key in the thing configuration");
258 // store it current configuration and avoiding complete re-initialization via handleConfigurationUpdate
259 getLGWebOSConfig().key = key;
261 // persist the configuration change
262 Configuration configuration = editConfiguration();
263 configuration.put(LGWebOSBindingConstants.CONFIG_KEY, key);
264 updateConfiguration(configuration);
269 public void storeProperties(Map<String, String> properties) {
270 logger.debug("storeProperties {}", properties);
271 Map<String, String> map = editProperties();
272 map.putAll(properties);
273 updateProperties(map);
277 public void onStateChanged(LGWebOSTVSocket.State state) {
280 postUpdate(CHANNEL_POWER, OnOffType.OFF);
283 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.tv-off");
284 channelHandlers.forEach((k, v) -> {
285 v.onDeviceRemoved(k, this);
286 v.removeAnySubscription(this);
296 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "@text/online.registering");
301 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "@text/online.connected");
303 channelHandlers.forEach((k, v) -> {
304 // refresh subscriptions except on channel, which can only be subscribe in livetv app. see
306 if (!CHANNEL_CHANNEL.equals(k)) {
307 v.refreshSubscription(k, this);
309 v.onDeviceReady(k, this);
318 public void onError(String error) {
319 logger.debug("Connection failed - error: {}", error);
321 switch (getSocket().getState()) {
328 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
329 String.format("@text/offline.comm-error-connexion-failed [ \"%s\" ]", error));
334 public void setOptions(String channelId, List<StateOption> options) {
335 logger.debug("setOptions channelId={} options.size()={}", channelId, options.size());
336 stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), channelId), options);
339 public void postUpdate(String channelId, State state) {
340 if (isLinked(channelId)) {
341 updateState(channelId, state);
344 // channel subscription only works when livetv app is started,
345 // therefore we need to slightly delay the subscription
346 if (CHANNEL_APP_LAUNCHER.equals(channelId)) {
347 if (APP_ID_LIVETV.equals(state.toString())) {
348 scheduleChannelSubscriptionJob();
350 stopChannelSubscriptionJob();
355 private void scheduleChannelSubscriptionJob() {
356 ScheduledFuture<?> job = channelSubscriptionJob;
357 if (job == null || job.isCancelled()) {
358 logger.debug("Schedule channel subscription job");
359 channelSubscriptionJob = scheduler.schedule(
360 () -> channelHandlers.get(CHANNEL_CHANNEL).refreshSubscription(CHANNEL_CHANNEL, this),
361 CHANNEL_SUBSCRIPTION_DELAY_SECONDS, TimeUnit.SECONDS);
365 private void stopChannelSubscriptionJob() {
366 ScheduledFuture<?> job = channelSubscriptionJob;
367 if (job != null && !job.isCancelled()) {
368 logger.debug("Stop channel subscription job");
371 channelSubscriptionJob = null;
375 public Collection<Class<? extends ThingHandlerService>> getServices() {
376 return Set.of(LGWebOSActions.class);
380 * Make a best effort to automatically detect the MAC address of the TV.
381 * If this does not work automatically, users can still set it manually in the Thing config.
383 private void findMacAddress() {
384 LGWebOSConfiguration c = getLGWebOSConfig();
385 String host = c.getHost();
386 if (!host.isEmpty()) {
388 // validate host, so that no command can be injected
389 String macAddress = WakeOnLanUtility.getMACAddress(InetAddress.getByName(host).getHostAddress());
390 if (macAddress != null && !macAddress.equals(c.macAddress)) {
391 c.macAddress = macAddress;
392 // persist the configuration change
393 Configuration configuration = editConfiguration();
394 configuration.put(LGWebOSBindingConstants.CONFIG_MAC_ADDRESS, macAddress);
395 updateConfiguration(configuration);
397 } catch (UnknownHostException e) {
398 logger.debug("Unable to determine MAC address: {}", e.getMessage());
403 public List<String> reportApplications() {
404 return appLauncher.reportApplications(getThing().getUID());
407 public List<String> reportChannels() {
408 return ((TVControlChannel) channelHandlers.get(CHANNEL_CHANNEL)).reportChannels(getThing().getUID());