From 73e18424b90457a6b44a7bf2f75cb241fcd95201 Mon Sep 17 00:00:00 2001 From: Olivier Date: Sat, 15 Oct 2022 21:52:33 +0200 Subject: [PATCH] [tplinkrouter] Initial contribution (#13369) * Initial commit Signed-off-by: Olivier Marceau --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.tplinkrouter/NOTICE | 13 + .../README.md | 57 +++++ .../org.openhab.binding.tplinkrouter/pom.xml | 17 ++ .../src/main/feature/feature.xml | 9 + .../TpLinkRouterBindingConstants.java | 41 +++ .../internal/TpLinkRouterConfiguration.java | 30 +++ .../internal/TpLinkRouterHandler.java | 239 ++++++++++++++++++ .../internal/TpLinkRouterHandlerFactory.java | 55 ++++ .../internal/TpLinkRouterTelenetListener.java | 51 ++++ .../internal/TpLinkRouterTelnetConnector.java | 164 ++++++++++++ .../main/resources/OH-INF/binding/binding.xml | 9 + .../OH-INF/i18n/tplinkrouter.properties | 52 ++++ .../OH-INF/thing/channel-group-types.xml | 86 +++++++ .../resources/OH-INF/thing/thing-types.xml | 45 ++++ bundles/pom.xml | 1 + 17 files changed, 875 insertions(+) create mode 100644 bundles/org.openhab.binding.tplinkrouter/NOTICE create mode 100644 bundles/org.openhab.binding.tplinkrouter/README.md create mode 100644 bundles/org.openhab.binding.tplinkrouter/pom.xml create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterBindingConstants.java create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterConfiguration.java create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandler.java create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandlerFactory.java create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelenetListener.java create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelnetConnector.java create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/i18n/tplinkrouter.properties create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/channel-group-types.xml create mode 100644 bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/CODEOWNERS b/CODEOWNERS index 5b91214dddf..4b693ba5113 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -329,6 +329,7 @@ /bundles/org.openhab.binding.tibber/ @kjoglum /bundles/org.openhab.binding.tivo/ @mlobstein /bundles/org.openhab.binding.touchwand/ @roieg +/bundles/org.openhab.binding.tplinkrouter/ @olivierkeke /bundles/org.openhab.binding.tplinksmarthome/ @Hilbrand /bundles/org.openhab.binding.tr064/ @openhab/add-ons-maintainers /bundles/org.openhab.binding.tradfri/ @cweitkamp @kaikreuzer diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index be5f6fa7856..17696c83825 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1651,6 +1651,11 @@ org.openhab.binding.touchwand ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.tplinkrouter + ${project.version} + org.openhab.addons.bundles org.openhab.binding.tplinksmarthome diff --git a/bundles/org.openhab.binding.tplinkrouter/NOTICE b/bundles/org.openhab.binding.tplinkrouter/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.tplinkrouter/README.md b/bundles/org.openhab.binding.tplinkrouter/README.md new file mode 100644 index 00000000000..8d8aac4a92a --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/README.md @@ -0,0 +1,57 @@ +# tplinkrouter Binding + +The tplinkrouter Binding allows monitoring and controlling TP-Link routers. + +The binding uses a telnet connection to communicate with the router. + +At the moment only wifi part is supported and `TD-W9970` is the only model tested. +This binding may work with other TP-Link router provided that they use the same telnet API. + +## Supported Things + +This binding provides only the `router` Thing. + +## Thing Configuration + +### `router` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|-----------------------------------------------|---------|----------|----------| +| hostname | text | Hostname or IP address of the device | N/A | yes | no | +| port | integer | Port for telnet connection | 23 | no | no | +| username | text | Username to access the router (same as WebUI) | N/A | yes | no | +| password | text | Password to access the device (same as WebUI) | N/A | yes | no | +| refreshInterval | integer | Interval the device is polled in sec. | 60 | no | yes | + +## Channels + +| Channel | Type | Read/Write | Description | +|-----------------------|--------|------------|------------------------------------------| +| `wifi#status` | Switch | RW | State of the wifi | +| `wifi#ssid` | String | R | SSID of the wifi network | +| `wifi#bandwidth` | String | R | Bandwidth of the wifi network | +| `wifi#qss` | Switch | RW | Quick Security Setup of the wifi network | +| `wifi#secMode` | String | R | Security Mode of the wifi network | +| `wifi#authentication` | String | R | Authentication Mode of the wifi network | +| `wifi#encryption` | String | R | Encryption Mode of the wifi network | +| `wifi#key` | String | R | Password of the wifi network | + +## Full Example + +`.things` configuration file: + +``` +Thing tplinkrouter:router:myRouter [hostname="192.168.0.1", username="admin", password="myPassword"] +``` + +`.items` configuration file: + +``` +Switch Wifi "Wifi" {channel="tplinkrouter:router:myRouter:wifi#status", autoupdate="false"} +String WifiSSID "Wifi SSID" {channel="tplinkrouter:router:myRouter:wifi#ssid"} +String BandWidth "Wifi Bandwidth" {channel="tplinkrouter:router:myRouter:wifi#bandwidth"} +Switch QSS "Wifi QSS" {channel="tplinkrouter:router:myRouter:wifi#qss", autoupdate="false"} +String SecMode "Wifi Security Mode" {channel="tplinkrouter:router:myRouter:wifi#secMode"} +String Authentication "Wifi Authentication Mode" {channel="tplinkrouter:router:myRouter:wifi#authentication"} +String Encryption "Wifi Encryption Mode" {channel="tplinkrouter:router:myRouter:wifi#encryption"} +``` diff --git a/bundles/org.openhab.binding.tplinkrouter/pom.xml b/bundles/org.openhab.binding.tplinkrouter/pom.xml new file mode 100644 index 00000000000..1a8052da8a1 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.4.0-SNAPSHOT + + + org.openhab.binding.tplinkrouter + + openHAB Add-ons :: Bundles :: TpLinkRouter Binding + + diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/feature/feature.xml b/bundles/org.openhab.binding.tplinkrouter/src/main/feature/feature.xml new file mode 100644 index 00000000000..fbc5b4128f9 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.tplinkrouter/${project.version} + + diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterBindingConstants.java b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterBindingConstants.java new file mode 100644 index 00000000000..52665f34094 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterBindingConstants.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tplinkrouter.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link TpLinkRouterBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Olivier Marceau - Initial contribution + */ +@NonNullByDefault +public class TpLinkRouterBindingConstants { + + private static final String BINDING_ID = "tplinkrouter"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_ROUTER = new ThingTypeUID(BINDING_ID, "router"); + + // List of all Channel ids + public static final String WIFI_STATUS = "wifi#status"; + public static final String WIFI_SSID = "wifi#ssid"; + public static final String WIFI_BANDWIDTH = "wifi#bandwidth"; + public static final String WIFI_QSS = "wifi#qss"; + public static final String WIFI_SECMODE = "wifi#secMode"; + public static final String WIFI_AUTHENTICATION = "wifi#authentication"; + public static final String WIFI_ENCRYPTION = "wifi#encryption"; + public static final String WIFI_KEY = "wifi#key"; +} diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterConfiguration.java b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterConfiguration.java new file mode 100644 index 00000000000..6d39a7c678c --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tplinkrouter.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TpLinkRouterConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Olivier Marceau - Initial contribution + */ +@NonNullByDefault +public class TpLinkRouterConfiguration { + + public String hostname = ""; + public int port = 23; + public String username = ""; + public String password = ""; + public int refreshInterval = 60; +} diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandler.java b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandler.java new file mode 100644 index 00000000000..852fefe4071 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandler.java @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tplinkrouter.internal; + +import static org.openhab.binding.tplinkrouter.internal.TpLinkRouterBindingConstants.*; + +import java.io.IOException; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link TpLinkRouterHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Olivier Marceau - Initial contribution + */ +@NonNullByDefault +public class TpLinkRouterHandler extends BaseThingHandler implements TpLinkRouterTelenetListener { + + private static final long RECONNECT_DELAY = TimeUnit.MINUTES.toMillis(1); + + private static final String REFRESH_CMD = "wlctl show"; + private static final String WIFI_ON_CMD = "wlctl set --switch on"; + private static final String WIFI_OFF_CMD = "wlctl set --switch off"; + private static final String QSS_ON_CMD = "wlctl set --qss on"; + private static final String QSS_OFF_CMD = "wlctl set --qss off"; + + private final Logger logger = LoggerFactory.getLogger(TpLinkRouterHandler.class); + + private final TpLinkRouterTelnetConnector connector = new TpLinkRouterTelnetConnector(); + private final BlockingQueue commandQueue = new ArrayBlockingQueue<>(1); + + private TpLinkRouterConfiguration config = new TpLinkRouterConfiguration(); + private @Nullable ScheduledFuture scheduledFuture; + + public TpLinkRouterHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (WIFI_STATUS.equals(channelUID.getId())) { + try { + commandQueue.put(new ChannelUIDCommand(channelUID, command)); + } catch (InterruptedException e) { + logger.warn("Got exception", e); + Thread.currentThread().interrupt(); + } + if (command instanceof RefreshType) { + connector.sendCommand(REFRESH_CMD); + } else if (command == OnOffType.ON) { + connector.sendCommand(WIFI_ON_CMD); + } else if (command == OnOffType.OFF) { + connector.sendCommand(WIFI_OFF_CMD); + } + } else if (WIFI_QSS.equals(channelUID.getId())) { + try { + commandQueue.put(new ChannelUIDCommand(channelUID, command)); + } catch (InterruptedException e) { + logger.warn("Got exception", e); + Thread.currentThread().interrupt(); + } + if (command instanceof RefreshType) { + connector.sendCommand(REFRESH_CMD); + } else if (command == OnOffType.ON) { + connector.sendCommand(QSS_ON_CMD); + } else if (command == OnOffType.OFF) { + connector.sendCommand(QSS_OFF_CMD); + } + } + } + + @Override + public void initialize() { + config = getConfigAs(TpLinkRouterConfiguration.class); + updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(this::createConnection); + } + + @Override + public void dispose() { + ScheduledFuture scheduledFutureLocal = scheduledFuture; + if (scheduledFutureLocal != null) { + scheduledFutureLocal.cancel(true); + scheduledFuture = null; + } + commandQueue.clear(); + connector.dispose(); + super.dispose(); + } + + private void createConnection() { + connector.dispose(); + try { + connector.connect(this, config, this.getThing().getUID().getAsString()); + } catch (IOException e) { + logger.debug("Error while connecting, will retry in {} ms", RECONNECT_DELAY); + scheduler.schedule(this::createConnection, RECONNECT_DELAY, TimeUnit.MILLISECONDS); + } + } + + @Override + public void receivedLine(String line) { + logger.debug("Received line: {}", line); + Pattern pattern = Pattern.compile("(\\w+)=(.+)"); + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + String label = matcher.group(1); + String value = matcher.group(2); + switch (label) { + case "Status": + if ("Disabled".equals(value)) { + updateState(WIFI_STATUS, OnOffType.OFF); + } else if ("Up".equals(value)) { + updateState(WIFI_STATUS, OnOffType.ON); + } else { + logger.warn("Unsupported value {} for label {}", value, label); + } + break; + case "SSID": + updateState(WIFI_SSID, StringType.valueOf(value)); + break; + case "bandWidth": + updateState(WIFI_BANDWIDTH, StringType.valueOf(value)); + break; + case "QSS": + if ("Disabled".equals(value)) { + updateState(WIFI_QSS, OnOffType.OFF); + } else if ("Enable".equals(value)) { + updateState(WIFI_QSS, OnOffType.ON); + } else { + logger.warn("Unsupported value {} for label {}", value, label); + } + break; + case "SecMode": + String[] parts = value.split("\\s|-"); + updateState(WIFI_SECMODE, StringType.valueOf(parts[0])); + updateState(WIFI_AUTHENTICATION, StringType.valueOf(parts[1])); + if (parts.length >= 3) { + updateState(WIFI_ENCRYPTION, StringType.valueOf(parts[2])); + } else { + updateState(WIFI_ENCRYPTION, StringType.EMPTY); + } + break; + case "Key": + updateState(WIFI_KEY, StringType.valueOf(value)); + break; + default: + logger.debug("Unrecognized label {}", label); + } + } else if ("cmd:SUCC".equals(line)) { + ChannelUIDCommand channelUIDCommand = commandQueue.poll(); + if (channelUIDCommand != null && channelUIDCommand.getCommand() instanceof State) { + updateState(channelUIDCommand.getChannelUID(), (State) channelUIDCommand.getCommand()); + } + } else if ("Login incorrect. Try again.".equals(line)) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Login or password incorrect"); + } + } + + @Override + public void onReaderThreadStopped() { + updateStatus(ThingStatus.OFFLINE); + logger.debug("try to reconnect in {} ms", RECONNECT_DELAY); + scheduler.schedule(this::createConnection, RECONNECT_DELAY, TimeUnit.MILLISECONDS); + } + + @Override + public void onReaderThreadInterrupted() { + updateStatus(ThingStatus.OFFLINE); + } + + @Override + public void onReaderThreadStarted() { + scheduledFuture = scheduler.scheduleWithFixedDelay(() -> { + handleCommand(new ChannelUID(getThing().getUID(), WIFI_STATUS), RefreshType.REFRESH); + }, 0, config.refreshInterval, TimeUnit.SECONDS); + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void onCommunicationUnavailable() { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Connection not available. Check if there is not another open connection."); + } +} + +/** + * Stores a command with associated channel + * + * @author Olivier Marceau - Initial contribution + */ +@NonNullByDefault +class ChannelUIDCommand { + private final ChannelUID channelUID; + private final Command command; + + public ChannelUIDCommand(ChannelUID channelUID, Command command) { + this.channelUID = channelUID; + this.command = command; + } + + public ChannelUID getChannelUID() { + return channelUID; + } + + public Command getCommand() { + return command; + } +} diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandlerFactory.java b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandlerFactory.java new file mode 100644 index 00000000000..028bf0f877a --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterHandlerFactory.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tplinkrouter.internal; + +import static org.openhab.binding.tplinkrouter.internal.TpLinkRouterBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link TpLinkRouterHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Olivier Marceau - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.tplinkrouter", service = ThingHandlerFactory.class) +public class TpLinkRouterHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ROUTER); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_ROUTER.equals(thingTypeUID)) { + return new TpLinkRouterHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelenetListener.java b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelenetListener.java new file mode 100644 index 00000000000..2e00553afb5 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelenetListener.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tplinkrouter.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TpLinkRouterTelenetListener} defines listener for telnet events. + * + * @author Olivier Marceau - Initial contribution + */ +@NonNullByDefault +public interface TpLinkRouterTelenetListener { + + /** + * The telnet client has received a line. + * + * @param line the received line + */ + void receivedLine(String line); + + /** + * The telnet client encountered an IO error. + */ + void onReaderThreadStopped(); + + /** + * The telnet client has been interrupted. + */ + void onReaderThreadInterrupted(); + + /** + * The telnet client has successfully connected to the receiver. + */ + void onReaderThreadStarted(); + + /** + * The telnet socket is unavailable. + */ + void onCommunicationUnavailable(); +} diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelnetConnector.java b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelnetConnector.java new file mode 100644 index 00000000000..81984de44cd --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/java/org/openhab/binding/tplinkrouter/internal/TpLinkRouterTelnetConnector.java @@ -0,0 +1,164 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.tplinkrouter.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link TpLinkRouterTelnetConnector} is responsible for the telnet connection. + * + * @author Olivier Marceau - Initial contribution + */ +@NonNullByDefault +public class TpLinkRouterTelnetConnector { + + private static final int TIMEOUT_MS = (int) TimeUnit.MINUTES.toMillis(1); + + private final Logger logger = LoggerFactory.getLogger(TpLinkRouterTelnetConnector.class); + + private @Nullable Thread telnetClientThread; + private @Nullable Socket socket; // use raw socket since commons net usage seems discouraged + private @Nullable OutputStreamWriter out; + + public void connect(TpLinkRouterTelenetListener listener, TpLinkRouterConfiguration config, String thingUID) + throws IOException { + logger.debug("Connecting to {}", config.hostname); + + Socket socketLocal = new Socket(); + socketLocal.connect(new InetSocketAddress(config.hostname, config.port)); + socketLocal.setSoTimeout(TIMEOUT_MS); + socketLocal.setKeepAlive(true); + + InputStreamReader inputStream = new InputStreamReader(socketLocal.getInputStream()); + this.out = new OutputStreamWriter(socketLocal.getOutputStream()); + this.socket = socketLocal; + loginAttempt(listener, inputStream, config); + Thread clientThread = new Thread(() -> listenInputStream(listener, inputStream, config)); + clientThread.setName("OH-binding-" + thingUID); + this.telnetClientThread = clientThread; + clientThread.start(); + logger.debug("TP-Link router telnet client connected to {}", config.hostname); + } + + public void dispose() { + logger.debug("disposing connector"); + Thread clientThread = telnetClientThread; + if (clientThread != null) { + clientThread.interrupt(); + telnetClientThread = null; + } + Socket socketLocal = socket; + if (socketLocal != null) { + try { + socketLocal.close(); + } catch (IOException e) { + logger.debug("Error while disconnecting telnet client", e); + } + socket = null; + } + } + + public void sendCommand(String command) { + logger.debug("sending command: {}", command); + OutputStreamWriter output = out; + if (output != null) { + try { + output.write(command + '\n'); + output.flush(); + } catch (IOException e) { + logger.warn("Error sending command", e); + } + } else { + logger.debug("Cannot send command, no telnet connection"); + } + } + + private void listenInputStream(TpLinkRouterTelenetListener listener, InputStreamReader inputStream, + TpLinkRouterConfiguration config) { + listener.onReaderThreadStarted(); + BufferedReader in = new BufferedReader(inputStream); + try { + while (!Thread.currentThread().isInterrupted()) { + try { + String line = in.readLine(); + if (line != null && !line.isBlank()) { + listener.receivedLine(line); + if ("CLI exited after timing out".equals(line)) { + OutputStreamWriter output = out; + if (output != null) { + output.write("\n"); // trigger a "username:" prompt + output.flush(); + loginAttempt(listener, inputStream, config); + } + } + } + } catch (SocketTimeoutException e) { + logger.trace("Socket timeout"); + } + } + } catch (InterruptedIOException e) { + logger.debug("Error in telnet connection ", e); + } catch (IOException e) { + if (!Thread.currentThread().isInterrupted()) { + logger.debug("Error in telnet connection ", e); + listener.onReaderThreadStopped(); + } + } + if (Thread.currentThread().isInterrupted()) { + logger.debug("Interrupted client thread"); + listener.onReaderThreadInterrupted(); + } + } + + private void loginAttempt(TpLinkRouterTelenetListener listener, InputStreamReader inputStreamReader, + TpLinkRouterConfiguration config) throws IOException { + int charInt; + StringBuilder word = new StringBuilder(); + OutputStreamWriter output = out; + if (output != null) { + try { + while ((charInt = inputStreamReader.read()) != -1) { + word.append((char) charInt); + logger.trace("received char: {}", (char) charInt); + if (word.toString().contains("username:")) { + logger.debug("Sending username"); + output.write(config.username + '\n'); + output.flush(); + word = new StringBuilder(); + } + if (word.toString().contains("password:")) { + logger.debug("Sending password"); + output.write(config.password + '\n'); + output.flush(); + break; + } + } + } catch (SocketTimeoutException e) { + listener.onCommunicationUnavailable(); + } + } + } +} diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..905242ab0c8 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + TpLinkRouter Binding + This is the binding for controlling a TP-Link router with telnet. + + diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/i18n/tplinkrouter.properties b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/i18n/tplinkrouter.properties new file mode 100644 index 00000000000..2421030f531 --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/i18n/tplinkrouter.properties @@ -0,0 +1,52 @@ +# binding + +binding.tplinkrouter.name = TpLinkRouter Binding +binding.tplinkrouter.description = This is the binding for controlling a TP-Link router with telnet. + +# thing types + +thing-type.tplinkrouter.router.label = Router +thing-type.tplinkrouter.router.description = Router device monitored and controlled by telnet connection + +# thing types config + +thing-type.config.tplinkrouter.router.hostname.label = Hostname +thing-type.config.tplinkrouter.router.hostname.description = Hostname or IP address of the device +thing-type.config.tplinkrouter.router.password.label = Password +thing-type.config.tplinkrouter.router.password.description = Password to access the device +thing-type.config.tplinkrouter.router.port.label = Port +thing-type.config.tplinkrouter.router.port.description = Port for telnet connection +thing-type.config.tplinkrouter.router.refreshInterval.label = Refresh Interval +thing-type.config.tplinkrouter.router.refreshInterval.description = Interval the device is polled in sec. +thing-type.config.tplinkrouter.router.username.label = Username +thing-type.config.tplinkrouter.router.username.description = User to access the device + +# channel group types + +channel-group-type.tplinkrouter.wifiGroupType.label = Wifi +channel-group-type.tplinkrouter.wifiGroupType.description = Wifi channels + +# channel types + +channel-type.tplinkrouter.authentication.label = Wifi Authentication Mode +channel-type.tplinkrouter.authentication.state.option.AUTO = AUTO +channel-type.tplinkrouter.authentication.state.option.OPEN = OPEN +channel-type.tplinkrouter.authentication.state.option.SHARED = SHARED +channel-type.tplinkrouter.authentication.state.option.WPA = WPA +channel-type.tplinkrouter.authentication.state.option.WPA2 = WPA2 +channel-type.tplinkrouter.bandwidth.label = Wifi BandWidth +channel-type.tplinkrouter.bandwidth.state.option.Auto = Auto +channel-type.tplinkrouter.bandwidth.state.option.20M = 20 MHz +channel-type.tplinkrouter.bandwidth.state.option.40M = 40 MHz +channel-type.tplinkrouter.encryption.label = Wifi Encryption Mode +channel-type.tplinkrouter.encryption.description = Wifi Encryption Mode (only for PSK security mode) +channel-type.tplinkrouter.encryption.state.option.AUTO = AUTO +channel-type.tplinkrouter.encryption.state.option.TKIP = TKIP +channel-type.tplinkrouter.encryption.state.option.AES = AES +channel-type.tplinkrouter.key.label = Wifi Key +channel-type.tplinkrouter.qss.label = Wifi QSS +channel-type.tplinkrouter.security-mode.label = Wifi Security Mode +channel-type.tplinkrouter.security-mode.state.option.WEP = WEP +channel-type.tplinkrouter.security-mode.state.option.WPA = PSK +channel-type.tplinkrouter.ssid.label = Wifi SSID +channel-type.tplinkrouter.status.label = Wifi Status diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/channel-group-types.xml b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/channel-group-types.xml new file mode 100644 index 00000000000..85b5bb4d14b --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/channel-group-types.xml @@ -0,0 +1,86 @@ + + + + + + Wifi channels + + + + + + + + + + + + + + Switch + + + + Switch + + + + String + + + + + String + + + + + String + + + + + + + + + + String + + + + + + + + + + + + + String + + Wifi Encryption Mode (only for PSK security mode) + + + + + + + + + + String + + + + + + + + + + diff --git a/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..9d50694416f --- /dev/null +++ b/bundles/org.openhab.binding.tplinkrouter/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,45 @@ + + + + + + + Router device monitored and controlled by telnet connection + + + + + + + + network-address + + Hostname or IP address of the device + + + + Port for telnet connection + 23 + + + + User to access the device + + + password + + Password to access the device + + + + Interval the device is polled in sec. + 60 + true + + + + + diff --git a/bundles/pom.xml b/bundles/pom.xml index 8ed05c5ad8c..c6272d061a5 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -364,6 +364,7 @@ org.openhab.binding.tibber org.openhab.binding.tivo org.openhab.binding.touchwand + org.openhab.binding.tplinkrouter org.openhab.binding.tplinksmarthome org.openhab.binding.tr064 org.openhab.binding.tradfri