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
|
### `salus-it600-device` Channels
|
||||||
|
|
||||||
| Channel | Type | Read/Write | Description |
|
| Channel | Type | Read/Write | Description |
|
||||||
|-----------------------------|--------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|----------------------------|--------------------|------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| it600-temp-channel | Number:Temperature | RO | Current temperature in the room |
|
| temperature | Number:Temperature | RO | Current temperature in the room |
|
||||||
| it600-expected-temp-channel | Number:Temperature | RW | Sets the desired temperature in the room |
|
| expected-temperature | 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. |
|
| 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
|
## Full Example
|
||||||
|
|
||||||
|
@ -17,9 +17,6 @@ import java.util.Set;
|
|||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Martin Grześlowski - Initial contribution
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* The {@link SalusBindingConstants} class defines common constants, which are
|
* The {@link SalusBindingConstants} class defines common constants, which are
|
||||||
* used across the whole binding.
|
* used across the whole binding.
|
||||||
@ -65,6 +62,7 @@ public class SalusBindingConstants {
|
|||||||
public static final String TEMPERATURE = "temperature";
|
public static final String TEMPERATURE = "temperature";
|
||||||
public static final String EXPECTED_TEMPERATURE = "expected-temperature";
|
public static final String EXPECTED_TEMPERATURE = "expected-temperature";
|
||||||
public static final String WORK_TYPE = "work-type";
|
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";
|
public static final String GENERIC_OUTPUT_CHANNEL = "generic-output-channel";
|
||||||
|
@ -47,7 +47,7 @@ public final class AwsCloudBridgeHandler extends AbstractBridgeHandler<AwsCloudB
|
|||||||
@Override
|
@Override
|
||||||
public Set<String> it600RequiredChannels() {
|
public Set<String> it600RequiredChannels() {
|
||||||
return Set.of("ep9:sIT600TH:LocalTemperature_x100", "ep9:sIT600TH:HeatingSetpoint_x100",
|
return Set.of("ep9:sIT600TH:LocalTemperature_x100", "ep9:sIT600TH:HeatingSetpoint_x100",
|
||||||
"ep9:sIT600TH:HoldType");
|
"ep9:sIT600TH:HoldType", "ep9:sIT600TH:RunningState");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -45,7 +45,8 @@ public final class CloudBridgeHandler extends AbstractBridgeHandler<CloudBridgeC
|
|||||||
@Override
|
@Override
|
||||||
public Set<String> it600RequiredChannels() {
|
public Set<String> it600RequiredChannels() {
|
||||||
return Set.of("ep_9:sIT600TH:LocalTemperature_x100", "ep_9:sIT600TH:HeatingSetpoint_x100",
|
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
|
@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.AuthSalusApiException;
|
||||||
import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
|
import org.openhab.binding.salus.internal.rest.exceptions.SalusApiException;
|
||||||
import org.openhab.core.library.types.DecimalType;
|
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.QuantityType;
|
||||||
import org.openhab.core.library.types.StringType;
|
import org.openhab.core.library.types.StringType;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
import org.openhab.core.thing.ChannelUID;
|
||||||
@ -143,6 +144,9 @@ public class It600Handler extends BaseThingHandler {
|
|||||||
case WORK_TYPE:
|
case WORK_TYPE:
|
||||||
handleCommandForWorkType(channelUID, command);
|
handleCommandForWorkType(channelUID, command);
|
||||||
break;
|
break;
|
||||||
|
case RUNNING_STATE:
|
||||||
|
handleCommandForRunningState(channelUID, command);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logger.warn("Unknown channel `{}` for command `{}`", id, command);
|
logger.warn("Unknown channel `{}` for command `{}`", id, command);
|
||||||
}
|
}
|
||||||
@ -257,16 +261,35 @@ public class It600Handler extends BaseThingHandler {
|
|||||||
command.getClass().getSimpleName(), channelUID);
|
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)
|
private Optional<DeviceProperty.LongDeviceProperty> findLongProperty(String name, String shortName)
|
||||||
throws SalusApiException, AuthSalusApiException {
|
throws SalusApiException, AuthSalusApiException {
|
||||||
var deviceProperties = findDeviceProperties();
|
var deviceProperties = findDeviceProperties();
|
||||||
var property = deviceProperties.stream().filter(p -> p.getName().equals(name))
|
var property = deviceProperties.stream()//
|
||||||
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)
|
.filter(p -> p.getName().equals(name))//
|
||||||
.map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
|
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)//
|
||||||
|
.map(DeviceProperty.LongDeviceProperty.class::cast)//
|
||||||
|
.findAny();
|
||||||
if (property.isEmpty()) {
|
if (property.isEmpty()) {
|
||||||
property = deviceProperties.stream().filter(p -> p.getName().contains(shortName))
|
property = deviceProperties.stream()//
|
||||||
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)
|
.filter(p -> p.getName().contains(shortName))//
|
||||||
.map(DeviceProperty.LongDeviceProperty.class::cast).findAny();
|
.filter(DeviceProperty.LongDeviceProperty.class::isInstance)//
|
||||||
|
.map(DeviceProperty.LongDeviceProperty.class::cast)//
|
||||||
|
.findAny();
|
||||||
}
|
}
|
||||||
if (property.isEmpty()) {
|
if (property.isEmpty()) {
|
||||||
logger.debug("{}/{} property not found!", name, shortName);
|
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.companyCode.label = Company Code
|
||||||
thing-type.config.salus.salus-aws-bridge.group.aws.label = AWS
|
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.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.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.maxHttpRetries.description = How many times HTTP requests can be retried
|
||||||
thing-type.config.salus.salus-aws-bridge.password.label = Password
|
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.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.label = Expected Temperature
|
||||||
channel-type.salus.it600-expected-temp-channel.description = Sets the desired temperature in room
|
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.label = Temperature
|
||||||
channel-type.salus.it600-temp-channel.description = Current temperature in room
|
channel-type.salus.it600-temp-channel.description = Current temperature in room
|
||||||
channel-type.salus.it600-work-type-channel.label = Work Type
|
channel-type.salus.it600-work-type-channel.label = Work Type
|
||||||
|
@ -21,7 +21,11 @@
|
|||||||
<channel id="temperature" typeId="it600-temp-channel"/>
|
<channel id="temperature" typeId="it600-temp-channel"/>
|
||||||
<channel id="expected-temperature" typeId="it600-expected-temp-channel"/>
|
<channel id="expected-temperature" typeId="it600-expected-temp-channel"/>
|
||||||
<channel id="work-type" typeId="it600-work-type-channel"/>
|
<channel id="work-type" typeId="it600-work-type-channel"/>
|
||||||
|
<channel id="running-state" typeId="it600-running-state"/>
|
||||||
</channels>
|
</channels>
|
||||||
|
<properties>
|
||||||
|
<property name="thingTypeVersion">1</property>
|
||||||
|
</properties>
|
||||||
<representation-property>dsn</representation-property>
|
<representation-property>dsn</representation-property>
|
||||||
<config-description>
|
<config-description>
|
||||||
<parameter name="dsn" type="text" required="true">
|
<parameter name="dsn" type="text" required="true">
|
||||||
@ -70,4 +74,9 @@
|
|||||||
</options>
|
</options>
|
||||||
</state>
|
</state>
|
||||||
</channel-type>
|
</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>
|
</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