mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[salus] Add running-state
channel for it600 (#17221)
* ReverseEngineerProtocol Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
This commit is contained in:
parent
d18a6c1cda
commit
75b5a27455
84
bundles/org.openhab.binding.salus/DEV_README.md
Normal file
84
bundles/org.openhab.binding.salus/DEV_README.md
Normal file
@ -0,0 +1,84 @@
|
||||
# ReverseEngineerProtocol CLI Documentation
|
||||
|
||||
This documentation provides instructions on how to use the ReverseEngineerProtocol CLI program to reverse engineer the proprietary Salus protocol.
|
||||
|
||||
## How to Run
|
||||
|
||||
To execute the CLI program, run the `main` method from the `ReverseEngineerProtocol` class. You can either run it directly from an IDE or use the `java` command. The program requires three parameters: `email`, `password`, and the Salus backend type (`AwsSalusApi` or `HttpSalusApi`).
|
||||
|
||||
### Running from an IDE
|
||||
|
||||
1. Open the project in your IDE.
|
||||
2. Navigate to the `ReverseEngineerProtocol` class.
|
||||
3. Run the `main` method, passing in the required parameters.
|
||||
|
||||
### Running from the Command Line
|
||||
|
||||
```bash
|
||||
java -cp <your-compiled-class-path> ReverseEngineerProtocol <email> <password> <backendType>
|
||||
```
|
||||
|
||||
Replace `<your-compiled-class-path>` with the path to your compiled classes, and `<email>`, `<password>`, and `<backendType>` with your actual credentials and backend type.
|
||||
|
||||
## Methods
|
||||
|
||||
### `findDevices`
|
||||
|
||||
Finds and lists all devices associated with your Salus cloud account.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
./ReverseEngineerProtocol <email> <password> <backendType> findDevices
|
||||
```
|
||||
|
||||
### `findDeviceProperties <dsn>`
|
||||
|
||||
Retrieves all properties for the device with the given Device Serial Number (DSN).
|
||||
|
||||
**Parameters:**
|
||||
- `<dsn>`: The Device Serial Number of the target device.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
./ReverseEngineerProtocol <email> <password> <backendType> findDeviceProperties <dsn>
|
||||
```
|
||||
|
||||
### `findDeltaInProperties <dsn>`
|
||||
|
||||
Initializes by loading all properties from the given device, then filters out the properties that have changed or remained unchanged. This method is useful for identifying which property corresponds to a specific value or state.
|
||||
|
||||
**Parameters:**
|
||||
- `<dsn>`: The Device Serial Number of the target device.
|
||||
|
||||
**Example Use Case:**
|
||||
|
||||
To find which property stores the "running" state of a device:
|
||||
|
||||
1. Run `findDeltaInProperties <dsn>`.
|
||||
2. Filter out properties that have changed (this can be done multiple times).
|
||||
3. Trigger the device to change state (e.g., set the temperature higher than the current one to make the device run).
|
||||
4. Filter out properties that have not changed.
|
||||
5. Repeat steps 2-4 until the desired property is identified.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
./ReverseEngineerProtocol <email> <password> <backendType> findDeltaInProperties <dsn>
|
||||
```
|
||||
|
||||
### `monitorProperty <dsn> <propertyName> <sleep>`
|
||||
|
||||
Monitors and retrieves the value of a specific property from a given device at specified intervals.
|
||||
|
||||
**Parameters:**
|
||||
- `<dsn>`: The Device Serial Number of the target device.
|
||||
- `<propertyName>`: The name of the property to monitor.
|
||||
- `<sleep>`: (optional; default 1) The sleep interval (in seconds) between each check.
|
||||
|
||||
**Usage:**
|
||||
|
||||
```bash
|
||||
./ReverseEngineerProtocol <email> <password> <backendType> monitorProperty <dsn> <propertyName> <sleep>
|
||||
```
|
@ -75,11 +75,12 @@ removed.
|
||||
|
||||
### `salus-it600-device` Channels
|
||||
|
||||
| Channel | Type | Read/Write | Description |
|
||||
|-----------------------------|--------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| it600-temp-channel | Number:Temperature | RO | Current temperature in the room |
|
||||
| it600-expected-temp-channel | Number:Temperature | RW | Sets the desired temperature in the room |
|
||||
| it600-work-type-channel | String | RW | Sets the work type for the device. OFF - device is turned off MANUAL - schedules are turned off, following a manual temperature set, AUTOMATIC - schedules are turned on, following schedule, TEMPORARY_MANUAL - schedules are turned on, following manual temperature until the next schedule. |
|
||||
| Channel | Type | Read/Write | Description |
|
||||
|----------------------------|--------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| temperature | Number:Temperature | RO | Current temperature in the room |
|
||||
| expected-temperature | Number:Temperature | RW | Sets the desired temperature in the room |
|
||||
| work-type | String | RW | Sets the work type for the device. OFF - device is turned off MANUAL - schedules are turned off, following a manual temperature set, AUTOMATIC - schedules are turned on, following schedule, TEMPORARY_MANUAL - schedules are turned on, following manual temperature until the next schedule. |
|
||||
| running-state | Switch | RO | Is the device running |
|
||||
|
||||
## Full Example
|
||||
|
||||
|
@ -17,9 +17,6 @@ import java.util.Set;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* @author Martin Grześlowski - Initial contribution
|
||||
*/
|
||||
/**
|
||||
* The {@link SalusBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
@ -65,6 +62,7 @@ public class SalusBindingConstants {
|
||||
public static final String TEMPERATURE = "temperature";
|
||||
public static final String EXPECTED_TEMPERATURE = "expected-temperature";
|
||||
public static final String WORK_TYPE = "work-type";
|
||||
public static final String RUNNING_STATE = "running-state";
|
||||
}
|
||||
|
||||
public static final String GENERIC_OUTPUT_CHANNEL = "generic-output-channel";
|
||||
|
@ -47,7 +47,7 @@ public final class AwsCloudBridgeHandler extends AbstractBridgeHandler<AwsCloudB
|
||||
@Override
|
||||
public Set<String> it600RequiredChannels() {
|
||||
return Set.of("ep9:sIT600TH:LocalTemperature_x100", "ep9:sIT600TH:HeatingSetpoint_x100",
|
||||
"ep9:sIT600TH:HoldType");
|
||||
"ep9:sIT600TH:HoldType", "ep9:sIT600TH:RunningState");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -45,7 +45,8 @@ public final class CloudBridgeHandler extends AbstractBridgeHandler<CloudBridgeC
|
||||
@Override
|
||||
public Set<String> it600RequiredChannels() {
|
||||
return Set.of("ep_9:sIT600TH:LocalTemperature_x100", "ep_9:sIT600TH:HeatingSetpoint_x100",
|
||||
"ep_9:sIT600TH:SetHeatingSetpoint_x100", "ep_9:sIT600TH:HoldType", "ep_9:sIT600TH:SetHoldType");
|
||||
"ep_9:sIT600TH:SetHeatingSetpoint_x100", "ep_9:sIT600TH:HoldType", "ep_9:sIT600TH:SetHoldType",
|
||||
"ep_9:sIT600TH:RunningState");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -34,6 +34,7 @@ import org.openhab.binding.salus.internal.rest.DeviceProperty;
|
||||
import org.openhab.binding.salus.internal.rest.exceptions.AuthSalusApiException;
|
||||
import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
@ -143,6 +144,9 @@ public class It600Handler extends BaseThingHandler {
|
||||
case WORK_TYPE:
|
||||
handleCommandForWorkType(channelUID, command);
|
||||
break;
|
||||
case RUNNING_STATE:
|
||||
handleCommandForRunningState(channelUID, command);
|
||||
break;
|
||||
default:
|
||||
logger.warn("Unknown channel `{}` for command `{}`", id, command);
|
||||
}
|
||||
@ -257,16 +261,35 @@ public class It600Handler extends BaseThingHandler {
|
||||
command.getClass().getSimpleName(), channelUID);
|
||||
}
|
||||
|
||||
private void handleCommandForRunningState(ChannelUID channelUID, Command command)
|
||||
throws SalusApiException, AuthSalusApiException {
|
||||
if (!(command instanceof RefreshType)) {
|
||||
return;
|
||||
}
|
||||
findLongProperty(channelPrefix + ":sIT600TH:RunningState", "RunningState")//
|
||||
.map(DeviceProperty::getValue)//
|
||||
.map(value -> value > 0)//
|
||||
.map(OnOffType::from)//
|
||||
.ifPresent(state -> {
|
||||
updateState(channelUID, state);
|
||||
updateStatus(ONLINE);
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<DeviceProperty.LongDeviceProperty> findLongProperty(String name, String shortName)
|
||||
throws SalusApiException, AuthSalusApiException {
|
||||
var deviceProperties = findDeviceProperties();
|
||||
var property = deviceProperties.stream().filter(p -> p.getName().equals(name))
|
||||
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)
|
||||
.map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
|
||||
var property = deviceProperties.stream()//
|
||||
.filter(p -> p.getName().equals(name))//
|
||||
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)//
|
||||
.map(DeviceProperty.LongDeviceProperty.class::cast)//
|
||||
.findAny();
|
||||
if (property.isEmpty()) {
|
||||
property = deviceProperties.stream().filter(p -> p.getName().contains(shortName))
|
||||
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)
|
||||
.map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
|
||||
property = deviceProperties.stream()//
|
||||
.filter(p -> p.getName().contains(shortName))//
|
||||
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)//
|
||||
.map(DeviceProperty.LongDeviceProperty.class::cast)//
|
||||
.findAny();
|
||||
}
|
||||
if (property.isEmpty()) {
|
||||
logger.debug("{}/{} property not found!", name, shortName);
|
||||
|
@ -22,6 +22,7 @@ thing-type.config.salus.salus-aws-bridge.clientId.description = The app client I
|
||||
thing-type.config.salus.salus-aws-bridge.companyCode.label = Company Code
|
||||
thing-type.config.salus.salus-aws-bridge.group.aws.label = AWS
|
||||
thing-type.config.salus.salus-aws-bridge.group.aws.description = AWS Properties
|
||||
thing-type.config.salus.salus-aws-bridge.identityPoolId.label = Identity Pool ID
|
||||
thing-type.config.salus.salus-aws-bridge.maxHttpRetries.label = Max HTTP Retries
|
||||
thing-type.config.salus.salus-aws-bridge.maxHttpRetries.description = How many times HTTP requests can be retried
|
||||
thing-type.config.salus.salus-aws-bridge.password.label = Password
|
||||
@ -70,6 +71,8 @@ channel-type.salus.generic-output-number-channel.label = Generic Number Output
|
||||
channel-type.salus.generic-output-number-channel.description = This channel type represents a generic output. The channel is read-only and its state is represented as a numeric.
|
||||
channel-type.salus.it600-expected-temp-channel.label = Expected Temperature
|
||||
channel-type.salus.it600-expected-temp-channel.description = Sets the desired temperature in room
|
||||
channel-type.salus.it600-running-state.label = Running State
|
||||
channel-type.salus.it600-running-state.description = Is the device running
|
||||
channel-type.salus.it600-temp-channel.label = Temperature
|
||||
channel-type.salus.it600-temp-channel.description = Current temperature in room
|
||||
channel-type.salus.it600-work-type-channel.label = Work Type
|
||||
|
@ -21,7 +21,11 @@
|
||||
<channel id="temperature" typeId="it600-temp-channel"/>
|
||||
<channel id="expected-temperature" typeId="it600-expected-temp-channel"/>
|
||||
<channel id="work-type" typeId="it600-work-type-channel"/>
|
||||
<channel id="running-state" typeId="it600-running-state"/>
|
||||
</channels>
|
||||
<properties>
|
||||
<property name="thingTypeVersion">1</property>
|
||||
</properties>
|
||||
<representation-property>dsn</representation-property>
|
||||
<config-description>
|
||||
<parameter name="dsn" type="text" required="true">
|
||||
@ -70,4 +74,9 @@
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="it600-running-state">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Running State</label>
|
||||
<description>Is the device running</description>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||
|
||||
<thing-type uid="salus:salus-it600-device">
|
||||
<instruction-set targetVersion="1">
|
||||
<add-channel id="running-state">
|
||||
<type>salus:it600-running-state</type>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
</thing-type>
|
||||
|
||||
</update:update-descriptions>
|
@ -0,0 +1,404 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2024 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.salus.internal;
|
||||
|
||||
import static java.lang.Math.max;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Queue;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.eclipse.jetty.http2.client.HTTP2Client;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.openhab.binding.salus.internal.aws.http.AwsSalusApi;
|
||||
import org.openhab.binding.salus.internal.cloud.rest.HttpSalusApi;
|
||||
import org.openhab.binding.salus.internal.rest.Device;
|
||||
import org.openhab.binding.salus.internal.rest.DeviceProperty;
|
||||
import org.openhab.binding.salus.internal.rest.GsonMapper;
|
||||
import org.openhab.binding.salus.internal.rest.HttpClient;
|
||||
import org.openhab.binding.salus.internal.rest.exceptions.AuthSalusApiException;
|
||||
import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
|
||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* @author Martin Grześlowski - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ReverseEngineerProtocol implements AutoCloseable {
|
||||
static final Logger LOGGER = LoggerFactory.getLogger(ReverseEngineerProtocol.class);
|
||||
final List<String> methods = List.of("findDevices", "findDeviceProperties", "findDeltaInProperties",
|
||||
"monitorProperty");
|
||||
final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
|
||||
final String baseUrl = "https://service-api.eu.premium.salusconnect.io";
|
||||
final org.eclipse.jetty.client.HttpClient client = new org.eclipse.jetty.client.HttpClient(
|
||||
new SslContextFactory.Client());
|
||||
final HttpClientFactory httpClientFactory = new HttpClientFactory() {
|
||||
|
||||
@Override
|
||||
public org.eclipse.jetty.client.HttpClient createHttpClient(String consumerName) {
|
||||
throw new UnsupportedOperationException("ReverseEngineerProtocol.createHttpClient(consumerName)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.jetty.client.HttpClient createHttpClient(String consumerName,
|
||||
@Nullable SslContextFactory sslContextFactory) {
|
||||
throw new UnsupportedOperationException(
|
||||
"ReverseEngineerProtocol.createHttpClient(consumerName, sslContextFactory)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.eclipse.jetty.client.HttpClient getCommonHttpClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HTTP2Client createHttp2Client(String consumerName) {
|
||||
throw new UnsupportedOperationException("ReverseEngineerProtocol.createHttp2Client(consumerName)");
|
||||
}
|
||||
|
||||
@Override
|
||||
public HTTP2Client createHttp2Client(String consumerName, @Nullable SslContextFactory sslContextFactory) {
|
||||
throw new UnsupportedOperationException(
|
||||
"ReverseEngineerProtocol.createHttp2Client(consumerName, sslContextFactory)");
|
||||
}
|
||||
};
|
||||
final SalusApi api;
|
||||
|
||||
public ReverseEngineerProtocol(String username, String password, String apiType) throws Exception {
|
||||
requireNonNull(username);
|
||||
requireNonNull(password);
|
||||
requireNonNull(apiType);
|
||||
|
||||
client.start();
|
||||
var restClient = new HttpClient(client);
|
||||
var gsonMapper = new GsonMapper();
|
||||
if (apiType.equals(AwsSalusApi.class.getSimpleName())) {
|
||||
api = new AwsSalusApi(httpClientFactory, username, password.getBytes(StandardCharsets.UTF_8), baseUrl,
|
||||
restClient, gsonMapper, "eu-central-1_XGRz3CgoY", "60912c00-287d-413b-a2c9-ece3ccef9230",
|
||||
"4pk5efh3v84g5dav43imsv4fbj", "eu-central-1", "salus-eu", "a24u3z7zzwrtdl-ats");
|
||||
} else if (apiType.equals(HttpSalusApi.class.getSimpleName())) {
|
||||
api = new HttpSalusApi(username, password.getBytes(StandardCharsets.UTF_8), baseUrl, restClient, gsonMapper,
|
||||
Clock.systemDefaultZone());
|
||||
} else {
|
||||
printUsage();
|
||||
throw new IllegalStateException("Invalid api type: " + apiType);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length < 3) {
|
||||
printUsage();
|
||||
throw new IllegalStateException("Check usage");
|
||||
}
|
||||
|
||||
var runIndefinitely = args.length == 3;
|
||||
if (runIndefinitely) {
|
||||
LOGGER.info("Will run indefinitely, use ctrl-C to exit");
|
||||
}
|
||||
var queue = newQueue(args);
|
||||
try (var reverseProtocol = new ReverseEngineerProtocol(requireNonNull(queue.poll()),
|
||||
requireNonNull(queue.poll()), requireNonNull(queue.poll()))) {
|
||||
// noinspection LoopConditionNotUpdatedInsideLoop
|
||||
do {
|
||||
reverseProtocol.run(queue);
|
||||
} while (runIndefinitely);
|
||||
}
|
||||
LOGGER.info("Bye bye 👋");
|
||||
}
|
||||
|
||||
private static Queue<String> newQueue(String[] args) {
|
||||
var queue = new ArrayBlockingQueue<String>(args.length);
|
||||
queue.addAll(Arrays.asList(args));
|
||||
return queue;
|
||||
}
|
||||
|
||||
private void run(Queue<String> queue) throws Exception {
|
||||
var method = findMethod(queue);
|
||||
LOGGER.info("Will invoke method [" + method + "]");
|
||||
switch (method) {
|
||||
case "findDevices":
|
||||
findDevices();
|
||||
break;
|
||||
case "findDeviceProperties":
|
||||
findDeviceProperties(findDsn(queue));
|
||||
break;
|
||||
case "findDeltaInProperties":
|
||||
findDeltaInProperties(findDsn(queue));
|
||||
break;
|
||||
case "monitorProperty":
|
||||
monitorProperty(findDsn(queue), findPropertyName(queue, 6), findSleep(queue, 7));
|
||||
break;
|
||||
default:
|
||||
printUsage();
|
||||
throw new IllegalStateException("Invalid method: [" + method + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private String findMethod(Queue<String> args) throws IOException {
|
||||
var item = args.poll();
|
||||
if (item != null) {
|
||||
return item;
|
||||
}
|
||||
|
||||
int response = 0;
|
||||
while (response < 1 || response > methods.size()) {
|
||||
LOGGER.info(String.format("Please choose [method] 1-%d:", methods.size()));
|
||||
for (int i = 0; i < methods.size(); i++) {
|
||||
LOGGER.info(String.format("\t[%d]: %s", i + 1, methods.get(i)));
|
||||
}
|
||||
try {
|
||||
response = Integer.parseInt(reader.readLine());
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.info(e.getMessage());
|
||||
}
|
||||
}
|
||||
return methods.get(response - 1);
|
||||
}
|
||||
|
||||
private String findNextElement(String name, Queue<String> args) throws IOException {
|
||||
var item = args.poll();
|
||||
if (item != null) {
|
||||
return item;
|
||||
}
|
||||
LOGGER.info("Please pass [{}]:", name);
|
||||
var line = "";
|
||||
while (line == null || line.isEmpty()) {
|
||||
line = reader.readLine();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
private String findDsn(Queue<String> args) throws IOException {
|
||||
return findNextElement("dsn", args);
|
||||
}
|
||||
|
||||
private String findPropertyName(Queue<String> args, int idx) throws IOException {
|
||||
return findNextElement("propertyName", args);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Long findSleep(Queue<String> args, int idx) {
|
||||
var item = args.poll();
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(item);
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void printUsage() {
|
||||
LOGGER.info("""
|
||||
Usage:
|
||||
\tReverseEngineerProtocol <username> <password> <apiType> <method-name?> <params...>
|
||||
\tSupported method types:
|
||||
\t\tfindDevices
|
||||
\t\tfindDeviceProperties <dsn>
|
||||
\t\tfindDeltaInProperties <dsn>
|
||||
\t\tmonitorProperty <dsn> <propertyName> <sleepTime?>
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void findDevices() throws AuthSalusApiException, SalusApiException {
|
||||
var devices = api.findDevices();
|
||||
LOGGER.info(String.format("Your devices (%s):", api.getClass().getSimpleName()));
|
||||
printDevices(devices);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findDeviceProperties(String dsn) throws AuthSalusApiException, SalusApiException {
|
||||
var properties = api.findDeviceProperties(dsn);
|
||||
LOGGER.info(String.format("Properties for device %s (%s):", dsn, api.getClass().getSimpleName()));
|
||||
printDevicesProperties(properties);
|
||||
}
|
||||
|
||||
void findDeltaInProperties(String dsn) throws AuthSalusApiException, SalusApiException, IOException {
|
||||
requireNonNull(dsn);
|
||||
|
||||
var differentProperties = api.findDeviceProperties(dsn);
|
||||
var answer = "";
|
||||
while (true) {
|
||||
if (differentProperties.isEmpty()) {
|
||||
LOGGER.info("There are no more properties 😬...");
|
||||
break;
|
||||
}
|
||||
printDevicesProperties(differentProperties);
|
||||
|
||||
LOGGER.info("Read one more time and leave properties that changed (x) / not changed (q) or finish (f):");
|
||||
answer = reader.readLine();
|
||||
if (answer.equalsIgnoreCase("f")) {
|
||||
break;
|
||||
}
|
||||
if (!answer.equalsIgnoreCase("x") && !answer.equalsIgnoreCase("q")) {
|
||||
LOGGER.info("Wrong answer: " + answer);
|
||||
continue;
|
||||
}
|
||||
var changed = answer.equalsIgnoreCase("x");
|
||||
|
||||
var beforeSize = differentProperties.size();
|
||||
var currentProperties = api.findDeviceProperties(dsn);
|
||||
var oldProps = new TreeSet<>(differentProperties);
|
||||
differentProperties = currentProperties.stream()//
|
||||
.filter(currentProp -> filterProperties(oldProps, currentProp, changed))
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
var currentSize = differentProperties.size();
|
||||
var delta = beforeSize - currentSize;
|
||||
LOGGER.info(String.format("Current size: %d, beforeSize: %d, Δ: %d", currentSize, beforeSize, delta));
|
||||
}
|
||||
|
||||
LOGGER.info(String.format("Properties for device %s (%s):", dsn, api.getClass().getSimpleName()));
|
||||
if (differentProperties.isEmpty()) {
|
||||
LOGGER.info("None 😬...");
|
||||
} else {
|
||||
printDevicesProperties(differentProperties);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean filterProperties(SortedSet<DeviceProperty<?>> oldProps, DeviceProperty<?> currentProp,
|
||||
boolean changed) {
|
||||
return oldProps.stream()//
|
||||
.filter(p -> p.getName().equals(currentProp.getName()))//
|
||||
.anyMatch(p -> changed != Objects.equals(p.getValue(), currentProp.getValue()));
|
||||
}
|
||||
|
||||
void monitorProperty(String dsn, String propertyName, @Nullable Long sleep)
|
||||
throws AuthSalusApiException, SalusApiException, InterruptedException {
|
||||
requireNonNull(dsn);
|
||||
requireNonNull(propertyName);
|
||||
if (sleep == null) {
|
||||
sleep = 1L;
|
||||
}
|
||||
|
||||
LOGGER.info("Finish loop by ctrl+c");
|
||||
while (true) {
|
||||
var deviceProperty = api.findDeviceProperties(dsn).stream()//
|
||||
.filter(p -> p.getName().equals(propertyName))//
|
||||
.findAny();
|
||||
if (deviceProperty.isPresent()) {
|
||||
LOGGER.info(deviceProperty.get() + "");
|
||||
} else {
|
||||
LOGGER.info("Property does not exists!");
|
||||
break;
|
||||
}
|
||||
TimeUnit.SECONDS.sleep(sleep);
|
||||
}
|
||||
}
|
||||
|
||||
private void printDevices(Collection<Device> devices) {
|
||||
var sizeLength = String.valueOf(devices.size()).length();
|
||||
var longestDsn = max("dsn".length(),
|
||||
devices.stream().map(Device::dsn).mapToInt(String::length).max().orElse(0));
|
||||
var longestName = max("name".length(),
|
||||
devices.stream().map(Device::name).map(String::valueOf).mapToInt(String::length).max().orElse(0));
|
||||
var margins = 8;
|
||||
var pipe = "═".repeat(sizeLength + longestDsn + longestName + margins);
|
||||
System.out.printf("╔%s╦%s╦%s╗", "═".repeat(sizeLength + 2), "═".repeat(longestDsn + 2),
|
||||
"═".repeat(longestName + 2));
|
||||
System.out.printf("║ %s ║ %s ║ %s ║", rightAlign("#", sizeLength), leftAlign("name", longestDsn),
|
||||
leftAlign("value", longestName));
|
||||
System.out.printf("╠%s╬%s╬%s╣", "═".repeat(sizeLength + 2), "═".repeat(longestDsn + 2),
|
||||
"═".repeat(longestName + 2));
|
||||
|
||||
var idx = 1;
|
||||
for (var device : devices) {
|
||||
System.out.printf("║ %s ║ %s ║ %s ║", //
|
||||
rightAlign(String.valueOf(idx), sizeLength), //
|
||||
leftAlign(device.dsn(), longestDsn), //
|
||||
leftAlign(device.name(), longestName));
|
||||
idx++;
|
||||
}
|
||||
|
||||
System.out.printf("╚%s╩%s╩%s╝", "═".repeat(sizeLength + 2), "═".repeat(longestDsn + 2),
|
||||
"═".repeat(longestName + 2));
|
||||
}
|
||||
|
||||
private void printDevicesProperties(Collection<DeviceProperty<?>> properties) {
|
||||
var sizeLength = String.valueOf(properties.size()).length();
|
||||
var longestName = max("name".length(),
|
||||
properties.stream().map(DeviceProperty::getName).mapToInt(String::length).max().orElse(0));
|
||||
var longestValue = max("value".length(), properties.stream().map(DeviceProperty::getValue).map(String::valueOf)
|
||||
.mapToInt(String::length).max().orElse(0));
|
||||
var margins = 8;
|
||||
var pipe = "═".repeat(sizeLength + longestName + longestValue + margins);
|
||||
System.out.printf("╔%s╦%s╦%s╗", "═".repeat(sizeLength + 2), "═".repeat(longestName + 2),
|
||||
"═".repeat(longestValue + 2));
|
||||
System.out.printf("║ %s ║ %s ║ %s ║", rightAlign("#", sizeLength), leftAlign("name", longestName),
|
||||
leftAlign("value", longestValue));
|
||||
System.out.printf("╠%s╬%s╬%s╣", "═".repeat(sizeLength + 2), "═".repeat(longestName + 2),
|
||||
"═".repeat(longestValue + 2));
|
||||
|
||||
var idx = 1;
|
||||
for (var property : properties) {
|
||||
System.out.printf("║ %s ║ %s ║ %s ║", //
|
||||
rightAlign(String.valueOf(idx), sizeLength), //
|
||||
leftAlign(property.getName(), longestName), //
|
||||
leftAlign(property.getValue(), longestValue));
|
||||
idx++;
|
||||
}
|
||||
|
||||
System.out.printf("╚%s╩%s╩%s╝", "═".repeat(sizeLength + 2), "═".repeat(longestName + 2),
|
||||
"═".repeat(longestValue + 2));
|
||||
}
|
||||
|
||||
private String rightAlign(String inputString, int length) {
|
||||
if (inputString.length() >= length) {
|
||||
return inputString;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (sb.length() < length - inputString.length()) {
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(inputString);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String leftAlign(@Nullable Object obj, int length) {
|
||||
var inputString = String.valueOf(obj);
|
||||
if (inputString.length() >= length) {
|
||||
return inputString;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(inputString);
|
||||
while (sb.length() < length) {
|
||||
sb.append(' ');
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
client.stop();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user