diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 28893eca65d64..1a74f87bc94cf 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -121,6 +121,11 @@ org.openhab.binding.astro ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.asuswrt + ${project.version} + org.openhab.addons.bundles org.openhab.binding.atlona diff --git a/bundles/org.openhab.binding.asuswrt/NOTICE b/bundles/org.openhab.binding.asuswrt/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/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.asuswrt/README.md b/bundles/org.openhab.binding.asuswrt/README.md new file mode 100644 index 0000000000000..39dd664f304f4 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/README.md @@ -0,0 +1,91 @@ +# Asuswrt Binding + +This binding adds support to read information from ASUS-Routers (Copyright © ASUS). + +## Supported Things + +This binding supports ASUS routers with Asuswrt or [Asuswrt-Merlin](https://www.asuswrt-merlin.net/) firmware. +Firmware 5.x.x (some DSL models) is NOT supported (not Asuswrt). + +| ThingType | Name | Descripion | +|---------------|------------|--------------------------------------| +| bridge | router | Router to which the binding connects | +| - | interface | Network interface of the router | +| - | client | Client is connected to the bridge | + +### `router` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|---------------------------------------|---------------------|----------|----------| +| hostname | text | Hostname or IP address of the device | router.asus.com | yes | no | +| username | text | Username to access the device | N/A | yes | no | +| password | text | Password to access the device | N/A | yes | no | +| useSSL | boolean | Connect over SSL or use http:// | false | no | no | +| refreshInterval | integer | Interval the device is polled in sec. | 20 | no | yes | +| httpPort | integer | HTTP-Port | 80 | no | yes | +| httpsPort | integer | HTTPS-Port | 443 | no | yes | + +### `interface` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|---------------------------------------|---------------------|----------|----------| +| interfaceName | text | options name of interface (wan/lan) | N/A | yes | no | + +### `client` Thing Configuration + +| Name | Type | Description | Default | Required | Advanced | +|-----------------|---------|---------------------------------------|---------------------|----------|----------| +| macAddress | text | Unique MAC address of the device | N/A | yes | no | +| clientNick | text | Nickname used by OH | N/A | no | no | + + +## Properties + +All devices support some of the following properties: + +| property | description | things supporting this channel | +|------------------|------------------------------|---------------------------------------| +| vendor | Vendor of device | router, client | +| dnsName | DNS name of device | router, client | + + +## Channels + +All devices support some of the following channels: + +| group | channel |type | description | things supporting this channel | +|------------------|----------------------|------------------------|--------------------------------------------|-----------------------------------| +| network-info | mac-address | text (RO) | HW address | interface, client | +| | ip-address | text (RO) | IP address | interface | +| | ip-method | text (RO) | IP method (static/dhcp) | interface, client | +| | subnet | text (RO) | Subnetmask | interface | +| | gateway | text (RO) | Default gateway | interface | +| | dns-servers | text (RO) | DNS servers | interface | +| | network-state | Switch (RO) | Client is online | interface, client | +| | internet-state | Switch (RO) | Client connected to Internet | client | +| sys-info | mem-total | Number:DataAmountype | Total memory in MB | router | +| | mem-used | Number:DataAmountype | Used memory in MB | router | +| | mem-free | Number:DataAmountype | Free memory in MB | router | +| | mem-used-percent | Number:Dimensionles | Used memory in % | router | +| | cpu-used-percent | Number:Dimensionles | Total CPU usage in percent over all cores | router | +| client-list | known-clients | text (RO) | Known clients with name and MAC addresses | router | +| | online-clients | text (RO) | Online clients with name and MAC addresses | router | +| | online-macs | text (RO) | List with MAC addresses of online clients | router | +| | online-clients-count | Number:Dimensionless | Count of online clients | router | +| traffic | current-rx | Number:DataTransferRate| Current DataTransferRate MBits/s (receive) | interface, client | +| | current-tx | Number:DataTransferRate| Current DataTransferRate MBits/s (send) | interface, client | +| | today-rx | Number:DataAmount | Data received since 0:00 a clock in MB | interface, client | +| | today-tx | Number:DataAmount | Data sent since 0:00 a clock in MB | interface, client | +| | total-rx | Number:DataAmount | Data received since reboot in MB | interface, client | +| | total-tx | Number:DataAmount | Data sent since reboot in MB | interface, client | + + +## Events + +All devices support some of the following Events: + +| group | event |kind | description | things supporting this event | +|------------------|---------------------|------------|------------------------------------------------------------------------|---------------------------------| +| network-info | connection-event | Trigger | Fired if connection is established ('connected') or ('disconnected') | interface | +| | client-online-event | Trigger | Fired if client leaves ('gone') or enters ('connected') the network | client | +| client-list | client-online-event | Trigger | Fired if client leaves ('gone') or enters ('connected') the network | router | diff --git a/bundles/org.openhab.binding.asuswrt/pom.xml b/bundles/org.openhab.binding.asuswrt/pom.xml new file mode 100644 index 0000000000000..219096837055a --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.0.0-SNAPSHOT + + + org.openhab.binding.asuswrt + + openHAB Add-ons :: Bundles :: Asuswrt Binding + + diff --git a/bundles/org.openhab.binding.asuswrt/src/main/feature/feature.xml b/bundles/org.openhab.binding.asuswrt/src/main/feature/feature.xml new file mode 100644 index 0000000000000..73416289caf02 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/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.asuswrt/${project.version} + + diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/AsuswrtDiscoveryService.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/AsuswrtDiscoveryService.java new file mode 100644 index 0000000000000..252ced30bf16e --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/AsuswrtDiscoveryService.java @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal; + +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingConstants.*; +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingSettings.*; +import static org.openhab.binding.asuswrt.internal.helpers.AsuswrtUtils.*; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.asuswrt.internal.structures.AsuswrtClientInfo; +import org.openhab.binding.asuswrt.internal.structures.AsuswrtClientList; +import org.openhab.binding.asuswrt.internal.structures.AsuswrtInterfaceList; +import org.openhab.binding.asuswrt.internal.structures.AsuswrtIpInfo; +import org.openhab.binding.asuswrt.internal.things.AsuswrtRouter; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AsuswrtDiscoveryService} is responsible for discovering clients. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService { + private final Logger logger = LoggerFactory.getLogger(AsuswrtDiscoveryService.class); + private String uid = ""; + protected @NonNullByDefault({}) AsuswrtRouter router; + + public AsuswrtDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, DISCOVERY_TIMEOUT_S, false); + } + + @Override + public void activate() { + } + + @Override + public void deactivate() { + super.deactivate(); + removeAllResults(); + } + + @Override + public void setThingHandler(@Nullable ThingHandler handler) { + if (handler instanceof AsuswrtRouter router) { + router.setDiscoveryService(this); + this.router = router; + this.uid = router.getUID().getAsString(); + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return this.router; + } + + /* + * Scan handling + */ + + /** + * Starts a manual scan. + */ + @Override + public void startScan() { + logger.trace("{} starting scan", uid); + if (router != null) { + /* query Data */ + router.queryDeviceData(false); + /* discover interfaces */ + AsuswrtInterfaceList ifList = router.getInterfaces(); + handleInterfaceScan(ifList); + /* discover clients */ + AsuswrtClientList clientList = router.getClients(); + handleClientScan(clientList); + } + } + + @Override + public void stopScan() { + super.stopScan(); + removeOlderResults(getTimestampOfLastScan()); + } + + /** + * Removes all scan results. + */ + private void removeAllResults() { + removeOlderResults(new Date().getTime()); + } + + /** + * Creates {@link DiscoveryResult}s from the provided {@link AsuswrtInterfaceList}. + */ + private void handleInterfaceScan(AsuswrtInterfaceList ifList) { + try { + for (AsuswrtIpInfo ifInfo : ifList) { + DiscoveryResult discoveryResult = createInterfaceResult(ifInfo); + thingDiscovered(discoveryResult); + } + } catch (Exception e) { + logger.debug("Error while handling interface scan reults", e); + } + } + + /** + * Creates {@link DiscoveryResult}s from the provided {@link AsuswrtClientList}. + */ + public void handleClientScan(AsuswrtClientList clientList) { + try { + for (AsuswrtClientInfo client : clientList) { + DiscoveryResult discoveryResult = createClientResult(client); + thingDiscovered(discoveryResult); + } + } catch (Exception e) { + logger.debug("Error while handling client scan results", e); + } + } + + /* + * Discovery result creation + */ + + /** + * Creates a {@link DiscoveryResult} from the provided {@link AsuswrtIpInfo}. + */ + private DiscoveryResult createInterfaceResult(AsuswrtIpInfo interfaceInfo) { + String ifName = interfaceInfo.getName(); + String macAddress = interfaceInfo.getMAC(); + String label = "AwrtInterface_" + ifName; + + Map properties = new HashMap<>(); + properties.put(NETWORK_REPRESENTATION_PROPERTY, ifName); + properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress); + + logger.debug("{} thing discovered: '{}", uid, label); + if (this.router != null) { + ThingUID bridgeUID = router.getUID(); + ThingUID thingUID = new ThingUID(THING_TYPE_INTERFACE, bridgeUID, ifName); + return DiscoveryResultBuilder.create(thingUID).withProperties(properties) + .withRepresentationProperty(NETWORK_REPRESENTATION_PROPERTY).withBridge(bridgeUID).withLabel(label) + .build(); + } else { + ThingUID thingUID = new ThingUID(BINDING_ID, ifName); + return DiscoveryResultBuilder.create(thingUID).withProperties(properties) + .withRepresentationProperty(NETWORK_REPRESENTATION_PROPERTY).withLabel(label).build(); + } + } + + /** + * Creates a {@link DiscoveryResult} from the provided {@link AsuswrtClientInfo}. + */ + private DiscoveryResult createClientResult(AsuswrtClientInfo clientInfo) { + String macAddress = clientInfo.getMac(); + String unformatedMac = unformatMac(macAddress); + String clientName; + String nickName; + String label = "AwrtClient_"; + + // Create label and thing names + clientName = stringOrDefault(clientInfo.getName(), "client_" + unformatedMac); + nickName = stringOrDefault(clientInfo.getNickName(), clientName); + if (nickName.equals(clientName)) { + label += nickName; + } else { + label += nickName + " (" + clientName + ")"; + } + + // Create properties + Map properties = new HashMap<>(); + properties.put(Thing.PROPERTY_MAC_ADDRESS, macAddress); + properties.put(Thing.PROPERTY_VENDOR, clientInfo.getVendor()); + properties.put(PROPERTY_CLIENT_NAME, clientName); + properties.put(CHANNEL_CLIENT_NICKNAME, nickName); + + logger.debug("{} thing discovered: '{}", uid, label); + if (this.router != null) { + ThingUID bridgeUID = router.getUID(); + ThingUID thingUID = new ThingUID(THING_TYPE_CLIENT, bridgeUID, unformatedMac); + return DiscoveryResultBuilder.create(thingUID).withProperties(properties) + .withRepresentationProperty(CLIENT_REPRESENTATION_PROPERTY).withTTL(DISCOVERY_AUTOREMOVE_S) + .withBridge(bridgeUID).withLabel(label).build(); + } else { + ThingUID thingUID = new ThingUID(BINDING_ID, unformatedMac); + return DiscoveryResultBuilder.create(thingUID).withProperties(properties) + .withRepresentationProperty(CLIENT_REPRESENTATION_PROPERTY).withTTL(DISCOVERY_AUTOREMOVE_S) + .withLabel(label).build(); + } + } +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/AsuswrtHandlerFactory.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/AsuswrtHandlerFactory.java new file mode 100644 index 0000000000000..b52d3d0ecc1bc --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/AsuswrtHandlerFactory.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal; + +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingConstants.*; +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingSettings.*; +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtErrorConstants.*; + +import java.util.HashSet; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.openhab.binding.asuswrt.internal.things.AsuswrtClient; +import org.openhab.binding.asuswrt.internal.things.AsuswrtInterface; +import org.openhab.binding.asuswrt.internal.things.AsuswrtRouter; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +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.ComponentContext; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link AsuswrtHandlerFactory} is responsible for creating things and thing handlers. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.asuswrt", service = ThingHandlerFactory.class) +public class AsuswrtHandlerFactory extends BaseThingHandlerFactory { + private final Logger logger = LoggerFactory.getLogger(AsuswrtHandlerFactory.class); + private final Set routerHandlers = new HashSet<>(); + private final HttpClient httpClient; + + public AsuswrtHandlerFactory() { + // Set SslContextfactory + SslContextFactory sslContextFactory = new SslContextFactory.Client(); + if (HTTP_SSL_TRUST_ALL) { + sslContextFactory.setTrustAll(true); + sslContextFactory.setEndpointIdentificationAlgorithm(null); + } + // Create new httpClient + httpClient = new HttpClient(sslContextFactory); + httpClient.setFollowRedirects(false); + httpClient.setMaxConnectionsPerDestination(HTTP_MAX_CONNECTIONS); + httpClient.setMaxRequestsQueuedPerDestination(HTTP_MAX_QUEUED_REQUESTS); + try { + httpClient.start(); + } catch (Exception e) { + logger.error(ERR_HTTP_CLIENT_FAILED); + } + } + + @Deactivate + @Override + protected void deactivate(ComponentContext componentContext) { + super.deactivate(componentContext); + try { + httpClient.stop(); + } catch (Exception e) { + logger.debug("Unable to stop httpClient"); + } + } + + @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)) { + AsuswrtRouter router = new AsuswrtRouter((Bridge) thing, this.httpClient); + routerHandlers.add(router); + return router; + } else if (THING_TYPE_CLIENT.equals(thingTypeUID)) { + AsuswrtRouter router = getRouter(thing); + if (router != null) { + return new AsuswrtClient(thing, router); + } + } else if (THING_TYPE_INTERFACE.equals(thingTypeUID)) { + AsuswrtRouter router = getRouter(thing); + if (router != null) { + return new AsuswrtInterface(thing, router); + } + } + return null; + } + + /** + * Gets the {@link AsuswrtRouter} handler (Bridge) from a Thing. + */ + protected @Nullable AsuswrtRouter getRouter(Thing thing) { + ThingUID bridgeUID = thing.getBridgeUID(); + if (bridgeUID != null) { + for (AsuswrtRouter router : routerHandlers) { + if (bridgeUID.equals(router.getUID())) { + return router; + } + } + } + logger.warn(ERR_BRIDGE_NOT_DECLARED); + return null; + } +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtConnector.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtConnector.java new file mode 100644 index 0000000000000..f159881ec4a22 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtConnector.java @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.api; + +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingSettings.*; +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtErrorConstants.*; +import static org.openhab.binding.asuswrt.internal.helpers.AsuswrtUtils.getValueOrDefault; + +import java.net.NoRouteToHostException; +import java.util.concurrent.TimeoutException; + +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLKeyException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.api.ContentResponse; +import org.openhab.binding.asuswrt.internal.structures.AsuswrtConfiguration; +import org.openhab.binding.asuswrt.internal.structures.AsuswrtCredentials; +import org.openhab.binding.asuswrt.internal.things.AsuswrtRouter; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; + +/** + * The {@link AsuswrtConnector} is a {@link AsuswrtHttpClient} that also keeps track of router configuration and + * credentials. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtConnector extends AsuswrtHttpClient { + private final Logger logger = LoggerFactory.getLogger(AsuswrtConnector.class); + private AsuswrtCredentials credentials; + private AsuswrtConfiguration routerConfig; + protected Long lastQuery = 0L; + + public AsuswrtConnector(AsuswrtRouter router) { + super(router); + routerConfig = router.getConfiguration(); + this.credentials = new AsuswrtCredentials(routerConfig); + } + + /* + * Connector commands + */ + + /** + * Login to the router. + */ + public Boolean login() { + String url = getURL("login.cgi"); + String encodedCredentials = credentials.getEncodedCredentials(); + String payload = ""; + + logout(); // logout (unset cookie) first + router.errorHandler.reset(); + + logger.trace("({}) perform login to '{}' with '{}'", uid, url, encodedCredentials); + + payload = "login_authorization=" + encodedCredentials + "}"; + ContentResponse response = getSyncRequest(url, payload); + if (response != null) { + setCookieFromResponse(response); + } + if (cookieStore.isValid()) { + router.setState(ThingStatus.ONLINE); + return true; + } + return false; + } + + /** + * Logout and unsets the cookie. + */ + public void logout() { + this.cookieStore.resetCookie(); + } + + /** + * Gets system information from the device. + */ + public void querySysInfo(boolean asyncRequest) { + queryDeviceData(CMD_GET_SYSINFO, asyncRequest); + } + + /** + * Queries data from the device. + * + * @param command command constant to sent + * @param asyncRequest true if request should be sent asynchronous, false if synchronous + */ + public void queryDeviceData(String command, boolean asyncRequest) { + logger.trace("({}) queryDeviceData", uid); + Long now = System.currentTimeMillis(); + + router.errorHandler.reset(); + if (cookieStore.cookieIsExpired()) { + login(); + } + + if (now > this.lastQuery + HTTP_QUERY_MIN_GAP_MS) { + String url = getURL("appGet.cgi"); + String payload = "hook=" + command; + this.lastQuery = now; + + // Send payload as url parameter + url = url + "?" + payload; + url = url.replace(";", "%3B"); + + // Send asynchronous or synchronous HTTP request + if (asyncRequest) { + sendAsyncRequest(url, payload, command); + } else { + sendSyncRequest(url, payload, command); + } + } else { + logger.trace("({}) query skipped cause of min_gap: {} <- {}", uid, now, lastQuery); + } + } + + /* + * Response handling + */ + + /** + * Handle successful HTTP response by delegating to the connector class. + * + * @param responseBody response body as string + * @param command command constant which was sent + */ + @Override + protected void handleHttpSuccessResponse(String responseBody, String command) { + JsonObject jsonObject = getJsonFromString(responseBody); + router.dataReceived(jsonObject, command); + } + + /** + * Handles HTTP result failures. + * + * @param e Throwable exception + * @param payload full payload for debugging + */ + @Override + protected void handleHttpResultError(Throwable e, String payload) { + super.handleHttpResultError(e, payload); + String errorMessage = getValueOrDefault(e.getMessage(), ""); + + if (e instanceof TimeoutException || e instanceof NoRouteToHostException) { + router.errorHandler.raiseError(ERR_CONN_TIMEOUT, errorMessage); + router.setState(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage); + } else if (e instanceof SSLException || e instanceof SSLKeyException || e instanceof SSLHandshakeException) { + router.errorHandler.raiseError(ERR_SSL_EXCEPTION, payload); + router.setState(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage); + } else if (e instanceof InterruptedException) { + router.errorHandler.raiseError(new Exception(e), payload); + router.setState(ThingStatus.UNKNOWN, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage); + } else { + router.errorHandler.raiseError(new Exception(e), errorMessage); + router.setState(ThingStatus.UNKNOWN, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage); + } + } + + /* + * Other + */ + + /** + * Gets the target URL. + */ + protected String getURL(String site) { + String url = routerConfig.hostname; + if (routerConfig.useSSL) { + url = HTTPS_PROTOCOL + url; + if (routerConfig.httpsPort != 443) { + url = url + ":" + routerConfig.httpsPort; + } + } else { + url = HTTP_PROTOCOL + url; + if (routerConfig.httpPort != 80) { + url = url + ":" + routerConfig.httpPort; + } + } + return url + "/" + site; + } +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtCookie.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtCookie.java new file mode 100644 index 0000000000000..3e4c66a37d28f --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtCookie.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.api; + +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingSettings.COOKIE_LIFETIME_S; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AsuswrtCookie} is used for storing cookie details. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtCookie { + protected String cookie = ""; + protected String token = ""; + protected Long cookieTimeStamp = 0L; + + /* + * Set and reset functions + */ + + /** + * Sets a new cookie. + */ + public void setCookie(String cookie) { + this.cookie = cookie; + cookieTimeStamp = System.currentTimeMillis(); + } + + /** + * Resets a cookie. + */ + public void resetCookie() { + cookie = ""; + token = ""; + cookieTimeStamp = 0L; + } + + /* + * Cookie checks + */ + + /** + * Checks if a cookie is set. + */ + public boolean cookieIsSet() { + return !cookie.isBlank(); + } + + /** + * Checks if a cookie is expired. + * + * @return true if cookie is set and expired + */ + public boolean cookieIsExpired() { + return cookieTimeStamp > 0L && System.currentTimeMillis() > cookieTimeStamp + (COOKIE_LIFETIME_S * 1000); + } + + /** + * Checks if a cookie is set and not expired. + */ + public boolean isValid() { + return !cookieIsExpired() && cookieIsSet(); + } + + /** + * Gets the cookie. + */ + public String getCookie() { + return cookie; + } +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtHttpClient.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtHttpClient.java new file mode 100644 index 0000000000000..e8cd515f0252d --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/api/AsuswrtHttpClient.java @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.api; + +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingConstants.JSON_MEMBER_TOKEN; +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtBindingSettings.*; +import static org.openhab.binding.asuswrt.internal.constants.AsuswrtErrorConstants.*; +import static org.openhab.binding.asuswrt.internal.helpers.AsuswrtUtils.getValueOrDefault; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.asuswrt.internal.helpers.AsuswrtUtils; +import org.openhab.binding.asuswrt.internal.things.AsuswrtRouter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +/** + * The {@link AsuswrtHttpClient} is used for (a)synchronous HTTP requests. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtHttpClient { + private final Logger logger = LoggerFactory.getLogger(AsuswrtHttpClient.class); + private Gson gson = new Gson(); + protected AsuswrtRouter router; + protected final String uid; + public AsuswrtCookie cookieStore = new AsuswrtCookie(); + + public AsuswrtHttpClient(AsuswrtRouter router) { + this.router = router; + uid = router.getUID().toString(); + } + + /* + * HTTP actions + */ + + /** + * Sends a synchronous HTTP request. + * + * The result will be handled in {@link #handleHttpSuccessResponse(String, String) or + * {@link #handleHttpResultError(Throwable)}. + * + * If the response should be returned use {@link #getSyncRequest(String, String)} instead. + * + * @param url the URL the request is sent to + * @param payload the payload to send + * @param command the command to perform + */ + protected void sendSyncRequest(String url, String payload, String command) { + ContentResponse response = getSyncRequest(url, payload); + if (response != null) { + handleHttpSuccessResponse(response.getContentAsString(), command); + } + } + + /** + * Sends a synchronous HTTP request. + * + * @param url the URL the request is sent to + * @param payload the payload to send + * @return {@link ContentResponse} of the request + */ + protected @Nullable ContentResponse getSyncRequest(String url, String payload) { + logger.trace("({}) sendRequest '{}' to '{}' with cookie '{}'", uid, payload, url, cookieStore.getCookie()); + Request httpRequest = this.router.getHttpClient().newRequest(url).method(HttpMethod.POST.toString()); + + // Set header + httpRequest = setHeaders(httpRequest); + httpRequest.timeout(HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS); + + // Add request body + httpRequest.content(new StringContentProvider(payload, HTTP_CONTENT_CHARSET), HTTP_CONTENT_TYPE); + try { + return httpRequest.send(); + } catch (Exception e) { + handleHttpResultError(e); + } + return null; + } + + /** + * Sends an asynchronous HTTP request so it does not wait for an answer. + * + * The result will be handled in {@link #handleHttpSuccessResponse(String, String) or + * {@link #handleHttpResultError(Throwable)}. + * + * @param url the URL to which the request is sent to + * @param payload the payload data + * @param command command to execute, this will handle ResponseType + */ + protected void sendAsyncRequest(String url, String payload, String command) { + logger.trace("({}) sendAsyncRequest to '{}' with cookie '{}'", uid, url, cookieStore.getCookie()); + try { + Request httpRequest = router.getHttpClient().newRequest(url).method(HttpMethod.POST.toString()); + + // Set header + httpRequest = setHeaders(httpRequest); + + // Add request body + httpRequest.content(new StringContentProvider(payload, HTTP_CONTENT_CHARSET), HTTP_CONTENT_TYPE); + + httpRequest.timeout(HTTP_TIMEOUT_MS, TimeUnit.MILLISECONDS).send(new BufferingResponseListener() { + @NonNullByDefault({}) + @Override + public void onComplete(Result result) { + final HttpResponse response = (HttpResponse) result.getResponse(); + if (result.getFailure() != null) { + // Handle result errors + handleHttpResultError(result.getFailure()); + } else if (response.getStatus() != 200) { + logger.debug("({}) sendAsyncRequest response error '{}'", uid, response.getStatus()); + router.errorHandler.raiseError(ERR_RESPONSE, getContentAsString()); + } else { + // Request successful + String rBody = getContentAsString(); + logger.trace("({}) requestCompleted '{}'", uid, rBody); + // Handle result + handleHttpSuccessResponse(rBody, command); + } + } + }); + } catch (Exception e) { + router.errorHandler.raiseError(e); + } + } + + /** + * Sets HTTP headers. + */ + private Request setHeaders(Request httpRequest) { + // Set header + httpRequest.header("content-type", HTTP_CONTENT_TYPE); + httpRequest.header("user-agent", HTTP_USER_AGENT); + if (cookieStore.isValid()) { + httpRequest.header("cookie", cookieStore.getCookie()); + } + return httpRequest; + } + + /* + * Response handling + */ + + /** + * Handles HTTP result failures. + * + * @param e the exception + * @param payload full payload for debugging + */ + protected void handleHttpResultError(Throwable e, String payload) { + String errorMessage = getValueOrDefault(e.getMessage(), ""); + + if (e instanceof TimeoutException) { + logger.debug("({}) sendAsyncRequest timeout'{}'", uid, errorMessage); + } else if (e instanceof InterruptedException) { + logger.debug("({}) sending request interrupted: {}", uid, e.toString()); + } else { + logger.debug("({}) sendAsyncRequest failed'{}'", uid, errorMessage); + } + } + + protected void handleHttpResultError(Throwable e) { + handleHttpResultError(e, ""); + } + + /** + * Handles a successful HTTP response. + * + * @param responseBody response body as string + * @param command command constant which was sent + */ + protected void handleHttpSuccessResponse(String responseBody, String command) { + } + + /** + * Sets a cookie from a response. + */ + protected void setCookieFromResponse(ContentResponse response) { + cookieStore.resetCookie(); + if (response.getStatus() == 200) { + String rBody = response.getContentAsString(); + logger.trace("({}) received response '{}'", uid, rBody); + try { + /* get json object 'asus_token' */ + JsonObject jsonObject = gson.fromJson(rBody, JsonObject.class); + if (jsonObject != null && jsonObject.has(JSON_MEMBER_TOKEN)) { + String token = jsonObject.get(JSON_MEMBER_TOKEN).getAsString(); + this.cookieStore.setCookie("asus_token=" + token); + } + } catch (Exception e) { + logger.debug("({}) {} on login request '{}'", uid, ERR_RESPONSE, e.getMessage()); + router.errorHandler.raiseError(ERR_RESPONSE, e.getMessage()); + } + } else { + String reason = AsuswrtUtils.getValueOrDefault(response.getReason(), ""); + router.errorHandler.raiseError(ERR_RESPONSE, reason); + } + } + + /** + * Gets JSON from a response. + */ + protected JsonObject getJsonFromResponse(ContentResponse response) { + return getJsonFromString(response.getContentAsString()); + } + + /** + * Gets JSON from a response. + */ + protected JsonObject getJsonFromString(String responseBody) { + try { + JsonObject jsonObject = gson.fromJson(responseBody, JsonObject.class); + logger.trace("({}) received result: {}", uid, responseBody); + /* get error code (0=success) */ + if (jsonObject != null) { + return jsonObject; + } + } catch (Exception e) { + logger.debug("({}) {} {}", uid, ERR_JSON_FORMAT, responseBody); + router.getErrorHandler().raiseError(e); + } + return new JsonObject(); + } +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtBindingConstants.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtBindingConstants.java new file mode 100644 index 0000000000000..edcbdd2d0648a --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtBindingConstants.java @@ -0,0 +1,194 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.constants; + +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link AsuswrtBindingConstants} class defines common constants, which are used across the whole binding. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtBindingConstants { + + public static final String BINDING_ID = "asuswrt"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_ROUTER = new ThingTypeUID(BINDING_ID, "router"); + public static final ThingTypeUID THING_TYPE_CLIENT = new ThingTypeUID(BINDING_ID, "client"); + public static final ThingTypeUID THING_TYPE_INTERFACE = new ThingTypeUID(BINDING_ID, "interface"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ROUTER, THING_TYPE_CLIENT, + THING_TYPE_INTERFACE); + + // Things with channel groups + public static final Set CHANNEL_GROUP_THING_SET = Collections + .unmodifiableSet(Stream.of(SUPPORTED_THING_TYPES_UIDS).flatMap(Set::stream).collect(Collectors.toSet())); + + /* + * Channel lists + * Item channel names + */ + + // General event constants + public static final String EVENT_STATE_CONNECTED = "connected"; + public static final String EVENT_STATE_GONE = "gone"; + public static final String EVENT_STATE_DISCONNECTED = "disconnected"; + + // Global channels + public static final String CHANNELS_ALL = "any-channel"; + + // Channel group system info + public static final String CHANNEL_GROUP_SYSINFO = "sys-info"; + public static final String CHANNEL_MEM_FREE = "mem-free"; + public static final String CHANNEL_MEM_FREE_PERCENT = "mem-free-percent"; + public static final String CHANNEL_MEM_TOTAL = "mem-total"; + public static final String CHANNEL_MEM_USED = "mem-used"; + public static final String CHANNEL_MEM_USED_PERCENT = "mem-used-percent"; + public static final String CHANNEL_CPU_USED_PERCENT = "cpu-used-percent"; + + // Channel group interface information + public static final String CHANNEL_GROUP_NETWORK = "network-info"; + public static final String CHANNEL_NETWORK_IP = "ip-address"; + public static final String CHANNEL_NETWORK_MAC = "mac-address"; + public static final String CHANNEL_NETWORK_MASK = "subnet"; + public static final String CHANNEL_NETWORK_GATEWAY = "gateway"; + public static final String CHANNEL_NETWORK_METHOD = "ip-method"; + public static final String CHANNEL_NETWORK_DNS = "dns-servers"; + public static final String CHANNEL_NETWORK_STATE = "network-state"; + public static final String CHANNEL_NETWORK_INTERNET = "internet-state"; + public static final String EVENT_CONNECTION = "connection-event"; + + // Channel group clientList information + public static final String CHANNEL_GROUP_CLIENTS = "client-list"; + public static final String CHANNEL_CLIENTS_KNOWN = "known-clients"; + public static final String CHANNEL_CLIENTS_ONLINE = "online-clients"; + public static final String CHANNEL_CLIENTS_COUNT = "online-clients-count"; + public static final String CHANNEL_CLIENTS_ONLINE_MAC = "online-macs"; + public static final String EVENT_CLIENT_CONNECTION = "client-online-event"; + + // Channel group client information + public static final String CHANNEL_GROUP_CLIENT = "client"; + public static final String CHANNEL_CLIENT_NICKNAME = "client-name"; + + // Channel group traffic + public static final String CHANNEL_GROUP_TRAFFIC = "traffic"; + public static final String CHANNEL_TRAFFIC_TOTAL_RX = "total-rx"; + public static final String CHANNEL_TRAFFIC_TOTAL_TX = "total-tx"; + public static final String CHANNEL_TRAFFIC_TODAY_RX = "today-rx"; + public static final String CHANNEL_TRAFFIC_TODAY_TX = "today-tx"; + public static final String CHANNEL_TRAFFIC_CURRENT_RX = "current-rx"; + public static final String CHANNEL_TRAFFIC_CURRENT_TX = "current-tx"; + + /* + * Properties + */ + + // Interface + public static final String PROPERTY_INTERFACE_NAME = "interfaceName"; + public static final String NETWORK_REPRESENTATION_PROPERTY = "interfaceName"; + // client + public static final String PROPERTY_CLIENT_NAME = "dnsName"; + public static final String CLIENT_REPRESENTATION_PROPERTY = "macAddress"; + + /* + * JSON request member names + * Member names of JSON response + */ + public static final String JSON_MEMBER_TOKEN = "asus_token"; + // sysInfo + public static final String JSON_MEMBER_PRODUCTID = "productid"; + public static final String JSON_MEMBER_FIRMWARE = "firmver"; + public static final String JSON_MEMBER_BUILD = "buildno"; + public static final String JSON_MEMBER_EXTENDNO = "extendo"; + public static final String JSON_MEMBER_MAC = "lan_hwaddr"; + + // lanInfo + public static final String JSON_MEMBER_LAN_IP = "lan_ipaddr"; + public static final String JSON_MEMBER_LAN_GATEWAY = "lan_gateway"; + public static final String JSON_MEMBER_LAN_NETMASK = "lan_netmask"; + public static final String JSON_MEMBER_LAN_PROTO = "lan_proto"; + + // wanInfo + public static final String JSON_MEMBER_WAN_IP = "wanlink-ipaddr"; + public static final String JSON_MEMBER_WAN_GATEWAY = "wanlink-gateway"; + public static final String JSON_MEMBER_WAN_NETMASK = "wanlink-netmask"; + public static final String JSON_MEMBER_WAN_PROTO = "wanlink-type"; + public static final String JSON_MEMBER_WAN_DNS_SERVER = "wanlink-dns"; + public static final String JSON_MEMBER_WAN_CONNECTED = "wanlink-status"; + + // clientInfo + public static final String JSON_MEMBER_CLIENTS = "get_clientlist"; + public static final String JSON_MEMBER_MACLIST = "maclist"; + public static final String JSON_MEMBER_API_LEVEL = "ClientAPILevel"; + public static final String JSON_MEMBER_CLIENT_RXCUR = "curRx"; + public static final String JSON_MEMBER_CLIENT_TXCUR = "curTx"; + public static final String JSON_MEMBER_CLIENT_DEFTYPE = "defaultType"; + public static final String JSON_MEMBER_CLIENT_DPIDEVICE = "dpiDevice"; + public static final String JSON_MEMBER_CLIENT_DPITYPE = "dpiType"; + public static final String JSON_MEMBER_CLIENT_IPFROM = "from"; + public static final String JSON_MEMBER_CLIENT_GROUP = "group"; + public static final String JSON_MEMBER_CLIENT_INETMODE = "internetMode"; + public static final String JSON_MEMBER_CLIENT_INETSTATE = "internet-state"; + public static final String JSON_MEMBER_CLIENT_IP = "ip"; + public static final String JSON_MEMBER_CLIENT_IPMETHOD = "ip-method"; + public static final String JSON_MEMBER_CLIENT_IPGATEWAY = "isGateway"; + public static final String JSON_MEMBER_CLIENT_GN = "isGN"; + public static final String JSON_MEMBER_CLIENT_ITUNES = "isITunes"; + public static final String JSON_MEMBER_CLIENT_LOGIN = "isLogin"; + public static final String JSON_MEMBER_CLIENT_ONLINE = "isOnline"; + public static final String JSON_MEMBER_CLIENT_PRINTER = "isPrinter"; + public static final String JSON_MEMBER_CLIENT_WEBSRV = "isWebServer"; + public static final String JSON_MEMBER_CLIENT_WIFI = "isWL"; + public static final String JSON_MEMBER_CLIENT_KEEPARP = "keeparp"; + public static final String JSON_MEMBER_CLIENT_MAC = "mac"; + public static final String JSON_MEMBER_CLIENT_MACREPEAT = "macRepeat"; + public static final String JSON_MEMBER_CLIENT_NAME = "name"; + public static final String JSON_MEMBER_CLIENT_NICK = "nickName"; + public static final String JSON_MEMBER_CLIENT_MODE = "opMode"; + public static final String JSON_MEMBER_CLIENT_QOSLVL = "qosLevel"; + public static final String JSON_MEMBER_CLIENT_ROG = "ROG"; + public static final String JSON_MEMBER_CLIENT_RSSI = "rssi"; + public static final String JSON_MEMBER_CLIENT_SSID = "ssid"; + public static final String JSON_MEMBER_CLIENT_RXTOTAL = "totalRx"; + public static final String JSON_MEMBER_CLIENT_TXTOTAL = "totalTx"; + public static final String JSON_MEMBER_CLIENT_VENDOR = "vendor"; + public static final String JSON_MEMBER_CLIENT_CONNECTTIME = "wlConnectTime"; + public static final String JSON_MEMBER_CLIENT_WTFAST = "wtfast"; + + // usage + public static final String JSON_MEMBER_CPU_USAGE = "cpu_usage"; + public static final String JSON_MEMBER_CPU_TOTAL = "cpu{x}_total"; + public static final String JSON_MEMBER_CPU_USED = "cpu{x}_usage"; + public static final String JSON_MEMBER_MEM_USAGE = "memory_usage"; + public static final String JSON_MEMBER_MEM_TOTAL = "mem_total"; + public static final String JSON_MEMBER_MEM_USED = "mem_used"; + public static final String JSON_MEMBER_MEM_FREE = "mem_free"; + public static final Integer USAGE_CPU_COUNT = 4; // max count of CPU cores + + // traffic + public static final String JSON_MEMBER_TRAFFIC = "netdev"; + public static final String JSON_MEMBER_INET_RX = "INTERNET_rx"; + public static final String JSON_MEMBER_INET_TX = "INTERNET_tx"; + public static final String JSON_MEMBER_LAN_RX = "WIRED_rx"; + public static final String JSON_MEMBER_LAN_TX = "WIRED_tx"; + public static final String JSON_MEMBER_WLAN_RX = "WIRELESS{}_rx"; + public static final String JSON_MEMBER_WLAN_TX = "WIRELESS{}_tx"; +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtBindingSettings.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtBindingSettings.java new file mode 100644 index 0000000000000..f42798bbd37e9 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtBindingSettings.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.constants; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AsuswrtBindingSettings} class defines common settings constants, which are used across the whole binding. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtBindingSettings { + + // Binding settings + public static final Integer HTTP_MAX_CONNECTIONS = 10; // setMaxConnectionsPerDestination for HTTP-Client + public static final Integer HTTP_MAX_QUEUED_REQUESTS = 10; // setMaxRequestsQueuedPerDestination for HTTP-Client + public static final Integer HTTP_TIMEOUT_MS = 5000; // http request timeout + public static final Integer HTTP_QUERY_MIN_GAP_MS = 5000; // http minimun gap between query data requests + public static final String HTTP_CONTENT_TYPE = "application/x-www-form-urlencoded"; + public static final String HTTP_USER_AGENT = "asusrouter-Android-DUTUtil-1.0.0.3.58-163"; + public static final String HTTP_CONTENT_CHARSET = "utf-8"; + public static final String HTTP_PROTOCOL = "http://"; + public static final String HTTPS_PROTOCOL = "https://"; + public static final Boolean HTTP_SSL_TRUST_ALL = true; // trust all ssl-certs + + public static final Integer COOKIE_LIFETIME_S = 3600; // lifetime of login-cookie + public static final Integer POLLING_INTERVAL_S_MIN = 5; // minimum polling interval + public static final Integer POLLING_INTERVAL_S_DEFAULT = 20; // default polling interval + public static final Integer RECONNECT_INTERVAL_S = 30; // interval trying try to reconnect to router + public static final Integer DISCOVERY_TIMEOUT_S = 10; // discovery service timeout in s + public static final Integer DISCOVERY_AUTOREMOVE_S = 1800; // discovery service remove things after x seconds + + // List of device commands + public static final String CMD_GET_SYSINFO = "nvram_get(productid);nvram_get(firmver);nvram_get(buildno);nvram_get(extendno);nvram_get(lan_hwaddr);"; + public static final String CMD_GET_LANINFO = "nvram_get(lan_hwaddr);nvram_get(lan_ipaddr);nvram_get(lan_proto);nvram_get(lan_netmask);nvram_get(lan_gateway);"; + public static final String CMD_GET_WANINFO = "wanlink(status);wanlink(type);wanlink(ipaddr);wanlink(netmask);wanlink(gateway);wanlink(dns);wanlink(lease);wanlink(expires);"; + public static final String CMD_GET_CLIENTLIST = "get_clientlist();"; + public static final String CMD_GET_TRAFFIC = "netdev(appobj);"; + public static final String CMD_GET_UPTIME = "uptime();"; + public static final String CMD_GET_USAGE = "cpu_usage(appobj);memory_usage(appobj);"; + public static final String CMD_GET_MEMUSAGE = "memory_usage(appobj);"; + public static final String CMD_GET_CPUUSAGE = "cpu_usage(appobj);"; + + // List of interfaces + public static final String INTERFACE_WAN = "wan"; + public static final String INTERFACE_LAN = "lan"; + public static final String INTERFACE_WLAN = "wlan"; + public static final String INTERFACE_CLIENT = "client"; + public static final Set INTERFACE_LIST = Set.of(INTERFACE_WAN, INTERFACE_LAN); +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtErrorConstants.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtErrorConstants.java new file mode 100644 index 0000000000000..78baab6bf33a3 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/constants/AsuswrtErrorConstants.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.constants; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AsuswrtErrorConstants} class defines error constants, which are used across the whole binding. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtErrorConstants { + + public static final String ERR_HTTP_CLIENT_FAILED = "Starting 'httpClient' failed"; + public static final String ERR_CONN_TIMEOUT = "Connection timeout"; + public static final String ERR_RESPONSE = "Response not okay"; + public static final String ERR_JSON_FORMAT = "Unexpected or malfomrated JSON response"; + public static final String ERR_JSON_UNKNOWN_MEMBER = "JSON member not found"; + public static final String ERR_SSL_EXCEPTION = "SSL Exception"; + public static final String ERR_INVALID_MAC_ADDRESS = "Invalid MAC address"; + public static final String ERR_BRIDGE_OFFLINE = "Bridge is offline"; + public static final String ERR_BRIDGE_NOT_DECLARED = "Bridge not found or not declared"; +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/helpers/AsuswrtErrorHandler.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/helpers/AsuswrtErrorHandler.java new file mode 100644 index 0000000000000..577566619b014 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/helpers/AsuswrtErrorHandler.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.helpers; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * This class is used for handling errors. + * + * @author Christian Wild - Initial contribution + */ +@NonNullByDefault +public class AsuswrtErrorHandler { + private String errorMessage = ""; + private String infoMessage = ""; + + public AsuswrtErrorHandler() { + } + + public AsuswrtErrorHandler(Exception ex) { + raiseError(ex); + } + + /* + * Public functions + */ + + /** + * Raises a new error. + * + * @param exception the exception + */ + public void raiseError(Exception ex) { + raiseError(ex, ""); + } + + /** + * Raises a new error. + * + * @param exception the exception + * @param infoMessage optional info message + */ + public void raiseError(Exception ex, @Nullable String infoMessage) { + this.errorMessage = AsuswrtUtils.getValueOrDefault(ex.getMessage(), ""); + this.infoMessage = AsuswrtUtils.getValueOrDefault(infoMessage, ""); + } + + /** + * Raises a new error. + * + * @param errorMessage the error message + * @param infoMessage optional info message + */ + public void raiseError(String errorMessage, @Nullable String infoMessage) { + this.errorMessage = errorMessage; + this.infoMessage = AsuswrtUtils.getValueOrDefault(infoMessage, ""); + } + + /** + * Resets the error. + */ + public void reset() { + errorMessage = ""; + infoMessage = ""; + } + + /* + * Getters + */ + + /** + * Get the error message. + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * Get the info message. + */ + public String getInfoMessage() { + return infoMessage; + } +} diff --git a/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/helpers/AsuswrtUtils.java b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/helpers/AsuswrtUtils.java new file mode 100644 index 0000000000000..613797fac7897 --- /dev/null +++ b/bundles/org.openhab.binding.asuswrt/src/main/java/org/openhab/binding/asuswrt/internal/helpers/AsuswrtUtils.java @@ -0,0 +1,414 @@ +/** + * Copyright (c) 2010-2023 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.asuswrt.internal.helpers; + +import java.util.regex.Pattern; + +import javax.measure.Unit; +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; +import javax.measure.quantity.Time; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.State; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * The {@link AsuswrtUtils} contains utility helper functions. + * + * @author Christian Wild - Initial Initial contribution + */ +@NonNullByDefault +public class AsuswrtUtils { + private static final Pattern PATTERN_MAC_PAIRS = Pattern.compile("^([a-fA-F0-9]{2}[:\\.-]?){5}[a-fA-F0-9]{2}$"); + private static final Pattern PATTERN_MAC_TRIPLES = Pattern.compile("^([a-fA-F0-9]{3}[:\\.-]?){3}[a-fA-F0-9]{3}$"); + + /* + * Calculation utility methods + */ + + /** + * Limits a value between limits. + * + * @param value the value that should be limited + * @param lowerLimit will be returned if value is below + * @param upperLimit will be returned if value is higher + */ + public static int limitVal(@Nullable Integer value, int lowerLimit, int upperLimit) { + if (value == null || value < lowerLimit) { + return lowerLimit; + } else if (value > upperLimit) { + return upperLimit; + } + return value; + } + + /* + * Formatting utility methods + */ + + /** + * Returns a value or default value when the value is null. + * + * @param Type of value + * @param value the value which should be checked + * @param defaultValue the default value that will be returned when value is null + */ + public static T getValueOrDefault(@Nullable T value, T defaultValue) { + return value == null ? defaultValue : value; + } + + /** + * Formats a MAC address by replacing old separator characters and adding new ones. + * + * @param mac unformatted MAC address + * @param newSeparatorChar new separator characters (e.g. ":","-" ) + */ + public static String formatMac(String mac, char newSeparatorChar) { + String unformatedMac = unformatMac(mac); + String formatedMac = ""; + try { + formatedMac = unformatedMac.replaceAll("(.{2})", "$1" + newSeparatorChar).substring(0, 17); + return formatedMac; + } catch (Exception e) { + return mac; + } + } + + /** + * Unformats a MAC address. Removes all separator characters. + */ + public static String unformatMac(String rawMac) { + String mac = rawMac; + mac = mac.replace("-", ""); + mac = mac.replace(":", ""); + mac = mac.replace(".", ""); + mac = mac.replace(" ", ""); + return mac; + } + + /** + * Checks if a MAC address is valid. + */ + public static boolean isValidMacAddress(String mac) { + // MAC-Addresses usually are 6 * 2 hex nibbles separated by colons, + // but apparently it is legal to have 4 * 3 hex nibbles as well, + // and the separators can be any of : or - or . or nothing. + return (PATTERN_MAC_PAIRS.matcher(mac).find() || PATTERN_MAC_TRIPLES.matcher(mac).find()); + } + + /** + * Converts a hexadecimal String to a byte array. + */ + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + try { + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + } catch (Exception e) { + } + return data; + } + + /** + * Converts a {@link String} to a boolean. + * + * @param s the string to be converted ('0', '1', '-1', 'true', 'false') + * @param defVal default value if no match was found + */ + public static boolean stringToBool(@Nullable String s, boolean defVal) { + if (s == null) { + return defVal; + } else if ("1".equals(s) || "-1".equals(s)) { + return true; + } else if ("0".equals(s)) { + return false; + } else { + try { + return Boolean.parseBoolean(s); + } catch (Exception e) { + return defVal; + } + } + } + + /** + * Converts a {@link String} to an int. + * + * @param s the string to be converted + * @param defVal the default value if the string is not a number + */ + public static int stringToInteger(@Nullable String s, int defVal) { + if (s == null) { + return defVal; + } + try { + return Integer.parseInt(s); + } catch (Exception e) { + return defVal; + } + } + + /** + * Returns the provided string if it is not null, empty or blank. Otherwise the default value is + * returned. + * + * @param s the string to check + * @param defVal the default value + * @return the string or the default value + */ + public static String stringOrDefault(@Nullable String s, String defVal) { + if (s == null || s.isEmpty() || s.isBlank()) { + return defVal; + } + return s; + } + + /* + * JSON formatting + */ + + /** + * Checks if a String is valid JSON. + */ + public static boolean isValidJson(String json) { + try { + Gson gson = new Gson(); + JsonObject jsnObject = gson.fromJson(json, JsonObject.class); + return jsnObject != null; + } catch (Exception e) { + return false; + } + } + + /** + * Gets a {@link String} value from a {@link JsonObject}. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + * @param defVal the default value if the key does not exist + */ + public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name, String defVal) { + if (jsonObject != null && jsonObject.has(name)) { + return jsonObject.get(name).getAsString(); + } else { + return defVal; + } + } + + /** + * Gets a {@link String} value from a {@link JsonObject} using an empty String as default value. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + */ + public static String jsonObjectToString(@Nullable JsonObject jsonObject, String name) { + return jsonObjectToString(jsonObject, name, ""); + } + + /** + * Gets a boolean value from a {@link JsonObject}. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + * @param defVal the default value if the key does not exist + */ + public static boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name, boolean defVal) { + if (jsonObject != null && jsonObject.has(name)) { + JsonPrimitive o = jsonObject.getAsJsonPrimitive(name); + if (o.isBoolean()) { + return jsonObject.get(name).getAsBoolean(); + } else if (o.isNumber()) { + Integer iVal = jsonObject.get(name).getAsInt(); + return (iVal.equals(1) || iVal.equals(-1)); + } else if (o.isString()) { + String val = jsonObject.get(name).getAsString(); + return stringToBool(val, defVal); + } + } + return defVal; + } + + /** + * Gets a boolean value from a {@link JsonObject} using false as default value. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + */ + public static boolean jsonObjectToBool(@Nullable JsonObject jsonObject, String name) { + return jsonObjectToBool(jsonObject, name, false); + } + + /** + * Gets an int value from a {@link JsonObject}. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + * @param defVal the default value if the key does not exist + */ + public static int jsonObjectToInt(@Nullable JsonObject jsonObject, String name, int defVal) { + if (jsonObject != null && jsonObject.has(name)) { + JsonPrimitive o = jsonObject.getAsJsonPrimitive(name); + if (o.isNumber()) { + return jsonObject.get(name).getAsInt(); + } else if (o.isString()) { + String val = jsonObject.get(name).getAsString(); + return stringToInteger(val, defVal); + } + } + return defVal; + } + + /** + * Gets an int value from a {@link JsonObject} using 0 as default value. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + */ + public static int jsonObjectToInt(@Nullable JsonObject jsonObject, String name) { + return jsonObjectToInt(jsonObject, name, 0); + } + + /** + * Gets a {@link Number} value from a {@link JsonObject}. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + * @param defVal the default value if the key does not exist + */ + public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name, Number defVal) { + if (jsonObject != null && jsonObject.has(name)) { + return jsonObject.get(name).getAsNumber(); + } else { + return defVal; + } + } + + /** + * Gets a {@link Number} value from a {@link JsonObject} using 0 as default value. + * + * @param jsonObject the object that will be searched for the key + * @param name the name of the key containing the value + */ + public static Number jsonObjectToNumber(@Nullable JsonObject jsonObject, String name) { + return jsonObjectToNumber(jsonObject, name, 0); + } + + /* + * Type utility methods + */ + + /** + * Returns an {@link OnOffType} from a {@link Boolean}. + */ + public static OnOffType getOnOffType(@Nullable Boolean boolVal) { + return (boolVal != null ? boolVal ? OnOffType.ON : OnOffType.OFF : OnOffType.OFF); + } + + /** + * Returns an {@link OnOffType} from an {@link Integer}. + */ + public static OnOffType getOnOffType(Integer intVal) { + return intVal == 0 ? OnOffType.OFF : OnOffType.ON; + } + + /** + * Returns a {@link StringType} from a {@link String}. + */ + public static StringType getStringType(@Nullable String strVal) { + return new StringType(strVal != null ? strVal : ""); + } + + /** + * Returns a {@link DecimalType} from a {@link Double}. + */ + public static DecimalType getDecimalType(@Nullable Double numVal) { + return new DecimalType((numVal != null ? numVal : 0)); + } + + /** + * Returns a {@link DecimalType} from an {@link Integer}. + */ + public static DecimalType getDecimalType(@Nullable Integer numVal) { + return new DecimalType((numVal != null ? numVal : 0)); + } + + /** + * Returns a {@link DecimalType} from a {@link Long}. + */ + public static DecimalType getDecimalType(@Nullable Long numVal) { + return new DecimalType((numVal != null ? numVal : 0)); + } + + /** + * Returns a {@link PercentType} from an {@link Integer}. + */ + public static PercentType getPercentType(@Nullable Integer numVal) { + Integer val = limitVal(numVal, 0, 100); + return new PercentType(val); + } + + /** + * Returns a {@link HSBType} from {@link Integer}s. + * + * @param hue the hue color + * @param saturation the saturation (0-100) + * @param brightness the brightness (0-100) + */ + public static HSBType getHSBType(Integer hue, Integer saturation, Integer brightness) { + DecimalType h = new DecimalType(hue); + PercentType s = new PercentType(saturation); + PercentType b = new PercentType(brightness); + return new HSBType(h, s, b); + } + + /** + * Returns a {@link QuantityType} with the {@link Time} unit. + */ + public static QuantityType