[salus] Add running-state channel for it600 (#17221)

* ReverseEngineerProtocol

Signed-off-by: Martin Grześlowski <martin.grzeslowski@gmail.com>
This commit is contained in:
Martin 2024-08-24 09:49:23 +02:00 committed by GitHub
parent d18a6c1cda
commit 75b5a27455
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 553 additions and 16 deletions

View 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>
```

View File

@ -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

View File

@ -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";

View File

@ -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

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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();
}
}