added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
49
bundles/org.openhab.binding.networkupstools/.classpath
Normal file
49
bundles/org.openhab.binding.networkupstools/.classpath
Normal file
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="test" value="true"/>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" path="target/generated-sources/annotations">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.networkupstools/.project
Normal file
23
bundles/org.openhab.binding.networkupstools/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.networkupstools</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.networkupstools/NOTICE
Normal file
13
bundles/org.openhab.binding.networkupstools/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
125
bundles/org.openhab.binding.networkupstools/README.md
Normal file
125
bundles/org.openhab.binding.networkupstools/README.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Network UPS Tools Binding
|
||||
|
||||
The primary goal of the [Network UPS Tools](https://networkupstools.org/) (NUT) project is to provide support for power devices, such as uninterruptible power supplies (UPS), Power Distribution Units and Solar Controllers.
|
||||
|
||||
Network UPS Tools (NUT) provides many control and monitoring features, with a uniform control and management interface.
|
||||
More than 100 different manufacturers, and several thousands of models are compatible.
|
||||
|
||||
This binding lets you integrate NUT servers with openHAB.
|
||||
|
||||
## Supported Things
|
||||
|
||||
The binding can connect to multiple NUT instances.
|
||||
The thing is an `ups` thing.
|
||||
The thing supports a number of NUT features out-of-the-box and supports the option to configure additional channels to get other NUT variables.
|
||||
The thing queries the NUT server for the status of the UPS approximate every 3 seconds and updates the status when a change happens.
|
||||
When a change of the UPS status happens it will query the NUT server to update all linked channels.
|
||||
Outside the status change updates, all linked channels are updated at the user configured refresh time.
|
||||
|
||||
Some NUT variables are static in nature and are not suited for a channel.
|
||||
Some of these could change, like of firmware version.
|
||||
Therefore these properties are updated with a 1 hour frequency.
|
||||
The following NUT variables are read and added to the thing as properties:
|
||||
|
||||
| Property | Description
|
||||
|------------------|----------------------------------------
|
||||
| ups.firmware | UPS firmware
|
||||
| ups.firmware.aux | Auxiliary device firmware
|
||||
| ups.id | UPS system identifier
|
||||
| ups.mfr | UPS manufacturer
|
||||
| ups.mfr.date | UPS manufacturing date
|
||||
| ups.model | UPS model
|
||||
| ups.serial | UPS serial number
|
||||
|
||||
|
||||
## Discovery
|
||||
|
||||
Discovery is not supported.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
The thing configuration requires the name of the UPS device as configured on the NUT server.
|
||||
If the NUT service isn't running locally the ip address or domain name (FDQN) of the server running NUT must be configured.
|
||||
Optional, port, username and password might need to be configured if required.
|
||||
|
||||
| Parameter | Default | Mandatory | Description
|
||||
|-----------|-----------|----------|-------------
|
||||
| device | | Yes | UPS device name, `ups` for example
|
||||
| host | localhost | Yes | UPS server hostname
|
||||
| port | 3493 | No | UPS server port, 3493 for example
|
||||
| username | | No | UPS server username
|
||||
| password | | No | UPS server password
|
||||
| refresh | 60 | No | Refresh interval for channel updates in seconds
|
||||
|
||||
## Channels
|
||||
|
||||
The following channels are available:
|
||||
|
||||
| Channel Name | Item Type | Unit | Description | Advanced |
|
||||
|----------------------------|--------------------------|------|------------------------------------------------------------------------------------|---------------|
|
||||
| upsAlarm | String | | UPS alarms | no |
|
||||
| upsLoad | Number:Dimensionless | % | Load on UPS (percent) | yes |
|
||||
| upsPower | Number:Power | VA | Current value of apparent power (Volt-Amps) | yes |
|
||||
| upsRealpower | Number:Power | W | Current value of real power (Watts) | no |
|
||||
| upsStatus | String | | Status of the UPS: OFF, OL,OB,LB,RB,OVER,TRIM,BOOST,CAL,BYPASS,NULL | no |
|
||||
| upsTemperature | Number:Temperature | °C | UPS temperature (degrees C) | yes |
|
||||
| upsTestResult | String | | Results of last self test (opaque string) | yes |
|
||||
| inputCurrent | Number:ElectricCurrent | A | Input current (A) | yes |
|
||||
| inputCurrentStatus | String | | Status relative to the thresholds | yes |
|
||||
| inputLoad | Number:Dimensionless | % | Load on (ePDU) input (percent of full) | no |
|
||||
| inputRealpower | Number:Power | W | Current sum value of all (ePDU) phases real power (W) | yes |
|
||||
| inputQuality | String | | Input power quality (*** opaque) | yes |
|
||||
| inputTransferReason | String | | Reason for last transfer to battery (*** opaque) | yes |
|
||||
| inputVoltage | Number:ElectricPotential | V | Input voltage (V) | yes |
|
||||
| inputVoltageStatus | String | | Status relative to the thresholds | yes |
|
||||
| outputCurrent | Number:ElectricCurrent | A | Output current (A) | yes |
|
||||
| outputVoltage | Number:ElectricPotential | V | Output voltage (V) | yes |
|
||||
| batteryCharge | Number:Dimensionless | % | Battery charge (percent) | no |
|
||||
| batteryRuntime | Number:Time | s | Battery runtime (seconds) | no |
|
||||
| batteryVoltage | Number:ElectricPotential | V | Battery voltage (V) | yes |
|
||||
|
||||
### Dynamic Channels
|
||||
|
||||
Because there is a lot of variation in UPS features, the binding supports dynamically adding channels for features, not supported out-of-the-box.
|
||||
To get data from another NUT variable the channel needs to configured.
|
||||
Channels can be created with as type: `Number`, `Number:<Quantity>`, `String` or `Switch`.
|
||||
|
||||
The following channel properties are needed:
|
||||
|
||||
| Property | Description | Example
|
||||
|-----------------|--------------------------------|-----------------
|
||||
| networkupstools | Links to NUT variable | `networkupstools="input.voltage.low.warning"`
|
||||
| unit | The unit of Quantity Type data | `unit="V"`
|
||||
|
||||
## Full Example
|
||||
|
||||
|
||||
ups.things:
|
||||
|
||||
```
|
||||
Thing networkupstools:ups:ups1 [ device="ups", host="localhost", refresh=60 ]
|
||||
```
|
||||
|
||||
ups-with-channels.things:
|
||||
|
||||
```
|
||||
Thing networkupstools:ups:ups2 [ device="ups", host="localhost", refresh=60 ] {
|
||||
Channels:
|
||||
String : testResult "Test Result" [networkupstools="ups.test.result"]
|
||||
Number:Frequency : upsOutFreq "Output Frequency" [networkupstools="output.frequency", unit="Hz"]
|
||||
Number:ElectricPotential : upsLowVoltage "Low Voltage" [networkupstools="input.voltage.low.warning", unit="V"]
|
||||
Number:ElectricCurrent : upsLowCurrent "Low Current" [networkupstools="input.current.low.warning", unit="A"]
|
||||
}
|
||||
```
|
||||
|
||||
ups.items
|
||||
|
||||
```
|
||||
Number:Dimensionless ups_battery_charge "Battery Charge [%d %%]" {channel="networkupstools:ups:ups1:batteryCharge"}
|
||||
Number:ElectricCurrent ups_current "Input Current [%d mA]"{channel="networkupstools:ups:ups1:inputCurrent"}
|
||||
|
||||
String test_result "Test Result" {channel="networkupstools:ups:ups2:testResult"}
|
||||
Number:Frequency ups_out_freq "Output Frequency" {channel="networkupstools:ups:ups2:upsOutFreq"}
|
||||
Number:ElectricPotential ups_low_voltage "Low Voltage [%.1f V]" {channel="networkupstools:ups:ups2:upsLowVoltage"}
|
||||
Number:ElectricCurrent ups_low_current "Input Current [%d A]" {channel="networkupstools:ups:ups2:upsLowCurrent"}
|
||||
```
|
||||
17
bundles/org.openhab.binding.networkupstools/pom.xml
Normal file
17
bundles/org.openhab.binding.networkupstools/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.networkupstools</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: Network UPS Tools Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.networkupstools-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-networkupstools" description="Network UPS Tools Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.networkupstools/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Power;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
import tec.uom.se.format.SimpleUnitFormat;
|
||||
import tec.uom.se.unit.ProductUnit;
|
||||
import tec.uom.se.unit.Units;
|
||||
|
||||
/**
|
||||
* The {@link NUTBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NUTBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "networkupstools";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_UPS = new ThingTypeUID(BINDING_ID, "ups");
|
||||
|
||||
public static final String METADATA_NETWORKUPSTOOLS = "networkupstools";
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_DYNAMIC_NUMBER = new ChannelTypeUID(BINDING_ID, "number");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_DYNAMIC_STRING = new ChannelTypeUID(BINDING_ID, "string");
|
||||
public static final ChannelTypeUID CHANNEL_TYPE_DYNAMIC_SWITCH = new ChannelTypeUID(BINDING_ID, "switch");
|
||||
public static final URI DYNAMIC_CHANNEL_CONFIG_QUANTITY_TYPE = URI
|
||||
.create("channel-type:ups:dynamic-channel-config-quantity-type");
|
||||
|
||||
public static final Unit<Power> AMPERE_PER_HOUR = new ProductUnit<>(Units.AMPERE.divide(Units.HOUR));
|
||||
public static final Unit<Power> VOLT_AMPERE = new ProductUnit<>(Units.VOLT.multiply(Units.AMPERE));
|
||||
|
||||
static {
|
||||
SimpleUnitFormat.getInstance().label(AMPERE_PER_HOUR, "Ah");
|
||||
SimpleUnitFormat.getInstance().label(VOLT_AMPERE, "VA");
|
||||
}
|
||||
|
||||
private static final String PARAMETER_PREFIX_UPS = "ups.";
|
||||
|
||||
/**
|
||||
* Enum with nut names which value will be set a parameter on the thing.
|
||||
* These are values that don't change at all (e.g. type of ups) or not very often (e.g. firmware version).
|
||||
*/
|
||||
public enum Parameters {
|
||||
UPS_FIRMWARE(PARAMETER_PREFIX_UPS + "firmware"),
|
||||
UPS_FIRMWARE_AUX(PARAMETER_PREFIX_UPS + "firmware.aux"),
|
||||
UPS_ID(PARAMETER_PREFIX_UPS + "id"),
|
||||
UPS_MFR(PARAMETER_PREFIX_UPS + "mfr"),
|
||||
UPS_MFR_DATE(PARAMETER_PREFIX_UPS + "mfr.date"),
|
||||
UPS_MODEL(PARAMETER_PREFIX_UPS + "model"),
|
||||
UPS_SERIAL(PARAMETER_PREFIX_UPS + "serial");
|
||||
|
||||
private final String nutName;
|
||||
|
||||
private Parameters(final String nutName) {
|
||||
this.nutName = nutName;
|
||||
}
|
||||
|
||||
public String getNutName() {
|
||||
return nutName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeProvider;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
|
||||
/**
|
||||
* Provider class to provide channel types for user configured channels.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NUTChannelTypeProvider implements ChannelTypeProvider, ThingHandlerService {
|
||||
|
||||
private final Map<ChannelTypeUID, ChannelType> map = new HashMap<>();
|
||||
private @Nullable ThingHandler handler;
|
||||
|
||||
@Override
|
||||
public Collection<ChannelType> getChannelTypes(@Nullable final Locale locale) {
|
||||
return Collections.unmodifiableCollection(map.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ChannelType getChannelType(final ChannelTypeUID channelTypeUID, @Nullable final Locale locale) {
|
||||
return map.get(channelTypeUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a channel type for a user configured channel.
|
||||
*
|
||||
* @param channelType channelType
|
||||
*/
|
||||
public void addChannelType(final ChannelType channelType) {
|
||||
map.put(channelType.getUID(), channelType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable final ThingHandler handler) {
|
||||
if (handler instanceof NUTHandler) {
|
||||
this.handler = handler;
|
||||
((NUTHandler) handler).setChannelTypeProvider(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link NUTConfiguration} class contains fields mapping thing configuration parameters.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NUTConfiguration {
|
||||
|
||||
/**
|
||||
* the refresh interval which is used to poll values from the NetworkUpsTools server.
|
||||
*/
|
||||
public int refresh = 60;
|
||||
public String device = "";
|
||||
public String host = "localhost";
|
||||
public String username = "";
|
||||
public String password = "";
|
||||
public int port = 3493;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Configuration class for dynamic created channels.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NUTDynamicChannelConfiguration {
|
||||
public String networkupstools = "";
|
||||
public @Nullable String unit;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import static org.openhab.binding.networkupstools.internal.NUTBindingConstants.BINDING_ID;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.binding.builder.ChannelBuilder;
|
||||
import org.openhab.core.thing.type.ChannelType;
|
||||
import org.openhab.core.thing.type.ChannelTypeBuilder;
|
||||
import org.openhab.core.thing.type.ChannelTypeUID;
|
||||
import org.openhab.core.types.StateDescriptionFragmentBuilder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Factory class to enrich dynamic created channels with additional configurations.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class NUTDynamicChannelFactory {
|
||||
|
||||
private static final String QUANTITY_ITEM_TYPE_PREFIX = CoreItemFactory.NUMBER + ':';
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NUTDynamicChannelFactory.class);
|
||||
|
||||
private final NUTChannelTypeProvider channelTypeProvider;
|
||||
|
||||
public NUTDynamicChannelFactory(final NUTChannelTypeProvider channelTypeProvider) {
|
||||
this.channelTypeProvider = channelTypeProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches the channel from the given channel and returns the newly created channel.
|
||||
*
|
||||
* @param channel Channel to enrich
|
||||
* @param channelConfig channel configuration
|
||||
* @return new created channel or null if it was not possible to create a new channel due to missing information or
|
||||
* otherwise
|
||||
*/
|
||||
public @Nullable Channel createChannel(final Channel channel, final NUTDynamicChannelConfiguration channelConfig) {
|
||||
final String acceptedItemType = channel.getAcceptedItemType();
|
||||
|
||||
if (acceptedItemType == null) {
|
||||
return null;
|
||||
}
|
||||
final ChannelTypeUID channelTypeUID;
|
||||
|
||||
if (acceptedItemType.startsWith(QUANTITY_ITEM_TYPE_PREFIX)) {
|
||||
channelTypeUID = createQuantityTypeChannel(channel, acceptedItemType, channelConfig);
|
||||
} else {
|
||||
channelTypeUID = getChannelTypeUID(acceptedItemType, channel.getUID());
|
||||
}
|
||||
return channelTypeUID == null ? null : ChannelBuilder.create(channel).withType(channelTypeUID).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ChannelTypeUID} for channels types that are supported and have a channel-type definition in
|
||||
* the binding thing xml.
|
||||
*
|
||||
* @param itemType item type to get the channel type for
|
||||
* @param channelUID ChannelUID for which the channel type is determined
|
||||
* @return channel type or null if not supported
|
||||
*/
|
||||
private @Nullable ChannelTypeUID getChannelTypeUID(final String itemType, final ChannelUID channelUID) {
|
||||
switch (itemType) {
|
||||
case CoreItemFactory.NUMBER:
|
||||
return NUTBindingConstants.CHANNEL_TYPE_DYNAMIC_NUMBER;
|
||||
case CoreItemFactory.STRING:
|
||||
return NUTBindingConstants.CHANNEL_TYPE_DYNAMIC_STRING;
|
||||
case CoreItemFactory.SWITCH:
|
||||
return NUTBindingConstants.CHANNEL_TYPE_DYNAMIC_SWITCH;
|
||||
default:
|
||||
logger.info("Dynamic channel '{}' is ignored because the type '{}' is not supported.", channelUID,
|
||||
itemType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ChannelTypeUID} for dynamically created {@link QuantityType} channels.
|
||||
* It registers the new channel type with the channel type provider.
|
||||
*
|
||||
* @param channel Channel to enrich
|
||||
* @param itemType item type to get the channel type for
|
||||
* @param channelConfig channel configuration
|
||||
* @return channel type or null if not supported
|
||||
*/
|
||||
private @Nullable ChannelTypeUID createQuantityTypeChannel(final Channel channel, final String itemType,
|
||||
final NUTDynamicChannelConfiguration channelConfig) {
|
||||
if (channelConfig.unit == null || channelConfig.unit.isEmpty()) {
|
||||
logger.info("Dynamic Channel '{}' is ignored because it's a QuantityType without a 'unit' property.",
|
||||
channel.getUID());
|
||||
return null;
|
||||
}
|
||||
final StateDescriptionFragmentBuilder sdb = StateDescriptionFragmentBuilder.create();
|
||||
final ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, channel.getUID().getId() + "Type");
|
||||
final String label = channel.getLabel();
|
||||
final ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, label == null ? "" : label, itemType)
|
||||
.withStateDescription(sdb.withReadOnly(Boolean.TRUE).build().toStateDescription())
|
||||
.withConfigDescriptionURI(NUTBindingConstants.DYNAMIC_CHANNEL_CONFIG_QUANTITY_TYPE).build();
|
||||
channelTypeProvider.addChannelType(channelType);
|
||||
return channelTypeUID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.networkupstools.internal.NUTBindingConstants.Parameters;
|
||||
import org.openhab.binding.networkupstools.internal.nut.NutApi;
|
||||
import org.openhab.binding.networkupstools.internal.nut.NutException;
|
||||
import org.openhab.binding.networkupstools.internal.nut.NutFunction;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
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.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.thing.binding.builder.ThingBuilder;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link NUTHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NUTHandler extends BaseThingHandler {
|
||||
private static final int REFRESH_RATE_SECONDS = 3;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NUTHandler.class);
|
||||
/**
|
||||
* Map to cache user configured channels with their configuration. Channels are dynamically created at
|
||||
* initialization phase of the thing.
|
||||
*/
|
||||
private final Map<ChannelUID, @Nullable NUTDynamicChannelConfiguration> userChannelToNutMap = new HashMap<>();
|
||||
/**
|
||||
* Cache of the UPS status. When expired makes a call to the NUT server is done to get the actual status. Expires at
|
||||
* the
|
||||
* short time refresh rate. Used to avoid triggering multiple calls to the server in a short time frame.
|
||||
*/
|
||||
private final ExpiringCache<String> upsStatusCache = new ExpiringCache<>(Duration.ofSeconds(REFRESH_RATE_SECONDS),
|
||||
this::retrieveUpsStatus);
|
||||
/**
|
||||
* Cache of the NUT variables. When expired makes a call to the NUT server is done to get the variables. Expires at
|
||||
* the short time refresh rate. Used to avoid triggering multiple calls to the server in a short time frame.
|
||||
*/
|
||||
private final ExpiringCache<Map<String, String>> variablesCache = new ExpiringCache<>(
|
||||
Duration.ofSeconds(REFRESH_RATE_SECONDS), this::retrieveVariables);
|
||||
/**
|
||||
* Cache used to manage update frequency of the thing properties. The properties are NUT variables that don't
|
||||
* change much or not at all. So updating can be done on a larger time frame. A call to get this cache will trigger
|
||||
* updating the properties when the cache is expired.
|
||||
*/
|
||||
private final ExpiringCache<Boolean> refreshPropertiesCache = new ExpiringCache<>(Duration.ofHours(1),
|
||||
this::updateProperties);
|
||||
private final ChannelUID upsStatusChannelUID;
|
||||
|
||||
private @Nullable NUTChannelTypeProvider channelTypeProvider;
|
||||
private @Nullable NUTDynamicChannelFactory dynamicChannelFactory;
|
||||
private @Nullable NUTConfiguration config;
|
||||
private @Nullable ScheduledFuture<?> poller;
|
||||
/**
|
||||
* Cache used to manage the update frequency of the thing channels. The channels are updated based on the user
|
||||
* configured refresh rate. The cache is called in the status update scheduled task and when the cache expires at
|
||||
* the user configured refresh rate it will trigger an update of the channels. This way no separate scheduled task
|
||||
* is needed.
|
||||
*/
|
||||
private @Nullable ExpiringCache<Boolean> refreshVariablesCache;
|
||||
private @Nullable NutApi nutApi;
|
||||
|
||||
/**
|
||||
* Keep track of the last ups status to avoid updating the status every 3 seconds when nothing changed.
|
||||
*/
|
||||
private String lastUpsStatus = "";
|
||||
|
||||
public NUTHandler(final Thing thing) {
|
||||
super(thing);
|
||||
upsStatusChannelUID = new ChannelUID(getThing().getUID(), NutName.UPS_STATUS.getChannelId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singleton(NUTChannelTypeProvider.class);
|
||||
}
|
||||
|
||||
public void setChannelTypeProvider(final NUTChannelTypeProvider channelTypeProvider) {
|
||||
this.channelTypeProvider = channelTypeProvider;
|
||||
dynamicChannelFactory = new NUTDynamicChannelFactory(channelTypeProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
final Channel channel = getThing().getChannel(channelUID);
|
||||
|
||||
if (channel == null) {
|
||||
logger.info("Trying to update a none existing channel: {}", channelUID);
|
||||
} else {
|
||||
updateChannel(channel, variablesCache.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
final NUTConfiguration config = getConfigAs(NUTConfiguration.class);
|
||||
|
||||
this.config = config;
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
initDynamicChannels();
|
||||
poller = scheduler.scheduleWithFixedDelay(this::refreshStatus, 0, REFRESH_RATE_SECONDS, TimeUnit.SECONDS);
|
||||
refreshVariablesCache = new ExpiringCache<>(Duration.ofSeconds(config.refresh), this::updateRefreshVariables);
|
||||
nutApi = new NutApi(config.host, config.port, config.username, config.password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (nutApi != null) {
|
||||
nutApi.close();
|
||||
}
|
||||
final ScheduledFuture<?> localPoller = poller;
|
||||
|
||||
if (localPoller != null && !localPoller.isCancelled()) {
|
||||
localPoller.cancel(true);
|
||||
poller = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes any channels configured by the user by creating complementary channel types and recreate the channels
|
||||
* of the thing.
|
||||
*/
|
||||
private void initDynamicChannels() {
|
||||
final NUTChannelTypeProvider localChannelTypeProvider = channelTypeProvider;
|
||||
final NUTDynamicChannelFactory localDynamicChannelFactory = dynamicChannelFactory;
|
||||
|
||||
if (localChannelTypeProvider == null || localDynamicChannelFactory == null) {
|
||||
return;
|
||||
}
|
||||
final List<Channel> updatedChannels = new ArrayList<>();
|
||||
boolean rebuildChannels = false;
|
||||
|
||||
for (final Channel channel : thing.getChannels()) {
|
||||
if (channel.getConfiguration().getProperties().isEmpty()) {
|
||||
updatedChannels.add(channel);
|
||||
} else {
|
||||
// If the channel has a custom created channel type id the channel should be recreated.
|
||||
// This is specific for Quantity type channels created in thing files.
|
||||
final boolean customChannel = channel.getChannelTypeUID() == null;
|
||||
final NUTDynamicChannelConfiguration channelConfig = channel.getConfiguration()
|
||||
.as(NUTDynamicChannelConfiguration.class);
|
||||
final Channel dynamicChannel;
|
||||
|
||||
rebuildChannels = customChannel;
|
||||
if (customChannel) {
|
||||
dynamicChannel = localDynamicChannelFactory.createChannel(channel, channelConfig);
|
||||
|
||||
if (dynamicChannel == null) {
|
||||
logger.debug("Could not initialize the dynamic channel '{}'. This channel will be ignored ",
|
||||
channel.getUID());
|
||||
continue;
|
||||
} else {
|
||||
logger.debug("Updating channel '{}' with dynamic channelType settings: {}", channel.getUID(),
|
||||
dynamicChannel.getChannelTypeUID());
|
||||
}
|
||||
} else {
|
||||
logger.debug("Mapping standard dynamic channel '{}' with dynamic channelType settings: {}",
|
||||
channel.getUID(), channel.getChannelTypeUID());
|
||||
dynamicChannel = channel;
|
||||
}
|
||||
userChannelToNutMap.put(channel.getUID(), channelConfig);
|
||||
updatedChannels.add(dynamicChannel);
|
||||
}
|
||||
}
|
||||
if (rebuildChannels) {
|
||||
final ThingBuilder thingBuilder = editThing();
|
||||
thingBuilder.withChannels(updatedChannels);
|
||||
updateThing(thingBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called by the scheduled task that checks for the active status of the ups.
|
||||
*/
|
||||
private void refreshStatus() {
|
||||
try {
|
||||
final String state = upsStatusCache.getValue();
|
||||
final ExpiringCache<Boolean> localVariablesRefreshCache = refreshVariablesCache;
|
||||
|
||||
if (!lastUpsStatus.equals(state)) {
|
||||
if (isLinked(upsStatusChannelUID)) {
|
||||
updateState(upsStatusChannelUID, state == null ? UnDefType.UNDEF : new StringType(state));
|
||||
}
|
||||
lastUpsStatus = state == null ? "" : state;
|
||||
if (localVariablesRefreshCache != null) {
|
||||
localVariablesRefreshCache.invalidateValue();
|
||||
}
|
||||
}
|
||||
// Just call a get on variables. If the cache is expired it will trigger an update of the channels.
|
||||
if (localVariablesRefreshCache != null) {
|
||||
localVariablesRefreshCache.getValue();
|
||||
}
|
||||
} catch (final RuntimeException e) {
|
||||
logger.debug("Updating ups status failed: ", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is triggered when the cache {@link #refreshVariablesCache} is expired.
|
||||
*
|
||||
* @return returns true if success and false on error
|
||||
*/
|
||||
private boolean updateRefreshVariables() {
|
||||
logger.trace("Calling updateRefreshVariables {}", thing.getUID());
|
||||
try {
|
||||
final Map<String, String> variables = variablesCache.getValue();
|
||||
|
||||
if (variables == null) {
|
||||
logger.trace("No data from NUT server received.");
|
||||
return false;
|
||||
}
|
||||
logger.trace("Updating status of linked channels.");
|
||||
for (final Channel channel : getThing().getChannels()) {
|
||||
final ChannelUID uid = channel.getUID();
|
||||
|
||||
if (isLinked(uid)) {
|
||||
updateChannel(channel, variables);
|
||||
}
|
||||
}
|
||||
// Call getValue to trigger cache refreshing
|
||||
refreshPropertiesCache.getValue();
|
||||
if ((thing.getStatus() == ThingStatus.OFFLINE || thing.getStatus() == ThingStatus.UNKNOWN)
|
||||
&& thing.getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
return true;
|
||||
} catch (final RuntimeException e) {
|
||||
logger.debug("Refresh Network UPS Tools failed: ", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that retrieves the ups status from the NUT server.
|
||||
*
|
||||
* @return status of the UPS or null if it couldn't be determined
|
||||
*/
|
||||
private @Nullable String retrieveUpsStatus() {
|
||||
final NutApi localNutApi = nutApi;
|
||||
|
||||
if (localNutApi == null) {
|
||||
return null;
|
||||
}
|
||||
return wrappedNutApiCall(device -> localNutApi.getVariable(device, NutName.UPS_STATUS.getName()), "UPS status");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that retrieves all variables from the NUT server.
|
||||
*
|
||||
* @return variables retrieved send by the NUT server or null if it couldn't be determined
|
||||
*/
|
||||
private @Nullable Map<String, String> retrieveVariables() {
|
||||
final NutApi localNutApi = nutApi;
|
||||
|
||||
if (localNutApi == null) {
|
||||
return null;
|
||||
}
|
||||
return wrappedNutApiCall(localNutApi::getVariables, "NUT variables");
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that wraps the call to the api and handles exceptions.
|
||||
*
|
||||
* @param <T> Return type of the call to the api
|
||||
* @param nutApiFunction function that will be called
|
||||
* @return the value returned by the api call or null in case of an error
|
||||
*/
|
||||
private <T> T wrappedNutApiCall(final NutFunction<String, T> nutApiFunction, String logging) {
|
||||
try {
|
||||
final NUTConfiguration localConfig = config;
|
||||
|
||||
if (localConfig == null) {
|
||||
return null;
|
||||
}
|
||||
logger.trace("Get {} from server for thing: {}({})", logging, thing.getLabel(), thing.getUID());
|
||||
return nutApiFunction.apply(localConfig.device);
|
||||
} catch (final NutException e) {
|
||||
logger.debug("Refresh Network UPS Tools failed: ", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the thing properties when the cache {@link #refreshPropertiesCache} is expired.
|
||||
*
|
||||
* @return returns true if success and false on error
|
||||
*/
|
||||
private Boolean updateProperties() {
|
||||
try {
|
||||
final Map<String, String> variables = variablesCache.getValue();
|
||||
|
||||
if (variables != null) {
|
||||
final Map<String, String> properties = editProperties();
|
||||
|
||||
for (final Parameters param : NUTBindingConstants.Parameters.values()) {
|
||||
final String value = variables.get(param.getNutName());
|
||||
|
||||
if (value == null) {
|
||||
logger.debug(
|
||||
"Variable '{}' intented as property for thing {}({}) is not available in the NUT data.",
|
||||
param.getNutName(), thing.getLabel(), thing.getUID());
|
||||
} else {
|
||||
properties.put(param.getNutName(), value);
|
||||
}
|
||||
}
|
||||
updateProperties(properties);
|
||||
}
|
||||
return Boolean.TRUE;
|
||||
} catch (final RuntimeException e) {
|
||||
logger.debug("Updating parameters failed: ", e);
|
||||
return Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannel(final Channel channel, @Nullable final Map<String, String> variables) {
|
||||
try {
|
||||
if (variables == null) {
|
||||
return;
|
||||
}
|
||||
final State state;
|
||||
final String id = channel.getUID().getId();
|
||||
final NutName fixedChannel = NutName.channelIdToNutName(id);
|
||||
|
||||
if (fixedChannel == null) {
|
||||
state = getDynamicChannelState(channel, variables);
|
||||
} else {
|
||||
state = fixedChannel.toState(variables);
|
||||
}
|
||||
updateState(channel.getUID(), state);
|
||||
} catch (final NutException | RuntimeException e) {
|
||||
logger.debug("Refresh Network UPS Tools failed: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
private State getDynamicChannelState(final Channel channel, @Nullable final Map<String, String> variables)
|
||||
throws NutException {
|
||||
final NUTDynamicChannelConfiguration nutConfig = userChannelToNutMap.get(channel.getUID());
|
||||
final String acceptedItemType = channel.getAcceptedItemType();
|
||||
|
||||
if (variables == null || acceptedItemType == null || nutConfig == null) {
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
final String value = variables.get(nutConfig.networkupstools);
|
||||
|
||||
if (value == null) {
|
||||
logger.info("Variable '{}' queried for thing {}({}) is not available in the NUT data.",
|
||||
nutConfig.networkupstools, thing.getLabel(), thing.getUID());
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
switch (acceptedItemType) {
|
||||
case CoreItemFactory.NUMBER:
|
||||
return new DecimalType(value);
|
||||
case CoreItemFactory.STRING:
|
||||
return StringType.valueOf(value);
|
||||
case CoreItemFactory.SWITCH:
|
||||
return OnOffType.from(value);
|
||||
default:
|
||||
if (acceptedItemType.startsWith(CoreItemFactory.NUMBER + ':')) {
|
||||
logger.debug("nut:{}, unit:{}, value:{}", nutConfig.networkupstools, nutConfig.unit, value);
|
||||
return new QuantityType<>(value + nutConfig.unit);
|
||||
}
|
||||
return UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import static org.openhab.binding.networkupstools.internal.NUTBindingConstants.THING_TYPE_UPS;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link NUTHandlerFactory} is responsible for creating things and thing handlers.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.networkupstools", service = ThingHandlerFactory.class)
|
||||
public class NUTHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_UPS);
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(final ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(final Thing thing) {
|
||||
final ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
return THING_TYPE_UPS.equals(thingTypeUID) ? new NUTHandler(thing) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
|
||||
/**
|
||||
* Supported NUT variables. Any NUT enum members have a complimentary channel definition in the XML thing definition.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
* @see https://github.com/networkupstools/nut/blob/master/docs/nut-names.txt
|
||||
*/
|
||||
enum NutName {
|
||||
// UPS
|
||||
UPS_ALARM("upsAlarm", "ups.alarm", StringType.class),
|
||||
UPS_LOAD("upsLoad", "ups.load", SmartHomeUnits.PERCENT),
|
||||
UPS_POWER("upsPower", "ups.power", NUTBindingConstants.VOLT_AMPERE),
|
||||
UPS_REALPOWER("upsRealpower", "ups.realpower", SmartHomeUnits.WATT),
|
||||
UPS_STATUS("upsStatus", "ups.status", StringType.class),
|
||||
UPS_TEMPERATURE("upsTemperature", "ups.temperature", SIUnits.CELSIUS),
|
||||
UPS_TEST_RESULT("upsTestResult", "ups.test.result", StringType.class),
|
||||
|
||||
// Input
|
||||
INPUT_CURRENT("inputCurrent", "input.current", SmartHomeUnits.AMPERE),
|
||||
INPUT_CURRENT_STATUS("inputCurrentStatus", "input.current.status", StringType.class),
|
||||
INPUT_LOAD("inputLoad", "input.load", SmartHomeUnits.PERCENT),
|
||||
INPUT_REALPOWER("inputRealpower", "input.realpower", SmartHomeUnits.WATT),
|
||||
INPUT_QUALITY("inputQuality", "input.quality", StringType.class),
|
||||
INPUT_TRANSFER_REASON("inputTransferReason", "input.transfer.reason", StringType.class),
|
||||
INPUT_VOLTAGE("inputVoltage", "input.voltage", SmartHomeUnits.VOLT),
|
||||
INPUT_VOLTAGE_STATUS("inputVoltageStatus", "input.voltage.status", StringType.class),
|
||||
|
||||
// Output
|
||||
OUTPUT_CURRENT("outputCurrent", "output.current", SmartHomeUnits.AMPERE),
|
||||
OUTPUT_VOLTAGE("outputVoltage", "output.voltage", SmartHomeUnits.VOLT),
|
||||
|
||||
// Battery
|
||||
BATTERY_CHARGE("batteryCharge", "battery.charge", SmartHomeUnits.PERCENT),
|
||||
BATTERY_RUNTIME("batteryRuntime", "battery.runtime", SmartHomeUnits.SECOND),
|
||||
BATTERY_VOLTAGE("batteryVoltage", "battery.voltage", SmartHomeUnits.VOLT);
|
||||
|
||||
private static final Map<String, NutName> NUT_NAME_MAP = Stream.of(NutName.values())
|
||||
.collect(Collectors.toMap(NutName::getChannelId, Function.identity()));
|
||||
|
||||
private final String channelId;
|
||||
private final String name;
|
||||
private final Class<? extends State> stateClass;
|
||||
private final Unit<?> unit;
|
||||
|
||||
NutName(final String channelId, final String name, final Class<? extends State> stateClass) {
|
||||
this(channelId, name, stateClass, null);
|
||||
}
|
||||
|
||||
NutName(final String channelId, final String name, final Unit<?> unit) {
|
||||
this(channelId, name, QuantityType.class, unit);
|
||||
}
|
||||
|
||||
NutName(final String channelId, final String name, final Class<? extends State> stateClass, final Unit<?> unit) {
|
||||
this.channelId = channelId;
|
||||
this.name = name;
|
||||
this.stateClass = stateClass;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the NUT enum for the given channel id or null if there is no NUT enum available for the given channel.
|
||||
*
|
||||
* @param channelId Channel to find the NUT enum for
|
||||
* @return The NUT enum or null if there is none.
|
||||
*/
|
||||
public static @Nullable NutName channelIdToNutName(final String channelId) {
|
||||
return NUT_NAME_MAP.get(channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link State} value of the variable for this NUT as is found in the given map of variables.
|
||||
*
|
||||
* @param channelId
|
||||
* @param variables Map of variables that contain a value for this NUT (or doesn't contain it if not available)
|
||||
* @return The {@link State} value or UNDEF if not available in the variables map or if it can't be determined.
|
||||
*/
|
||||
public static State toState(final String channelId, final Map<String, String> variables) {
|
||||
final NutName nutName = channelIdToNutName(channelId);
|
||||
|
||||
if (nutName instanceof NutName) {
|
||||
return nutName.toState(variables);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Channel name '" + channelId + "'is not a known data name");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link State} value of the variable for this NUT as is found in the given map of variables.
|
||||
*
|
||||
* @param variables Map of variables that contain a value for this NUT (or doesn't contain it if not available)
|
||||
* @return The {@link State} value or UNDEF if not available in the variables map or if it can't be determined.
|
||||
*/
|
||||
public State toState(final @Nullable Map<String, String> variables) {
|
||||
final State state;
|
||||
final String value = variables == null ? null : variables.get(name);
|
||||
|
||||
if (value == null) {
|
||||
state = UnDefType.UNDEF;
|
||||
} else {
|
||||
if (stateClass == StringType.class) {
|
||||
state = StringType.valueOf(value);
|
||||
} else if (stateClass == DecimalType.class) {
|
||||
state = DecimalType.valueOf(value);
|
||||
} else if (stateClass == PercentType.class) {
|
||||
state = PercentType.valueOf(value);
|
||||
} else if (stateClass == QuantityType.class) {
|
||||
state = QuantityType.valueOf(Double.valueOf(value), unit);
|
||||
} else {
|
||||
state = UnDefType.UNDEF;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The name of the Channel for this NUT variable as specified in the Thing
|
||||
*/
|
||||
String getChannelId() {
|
||||
return channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The variable name as used by the NUT server
|
||||
*/
|
||||
String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The {@link State} class type of this NUT variable
|
||||
*/
|
||||
Class<? extends State> getStateType() {
|
||||
return stateClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The {@link Unit} for this NUT variable if the type is a {@link QuantityType}
|
||||
*/
|
||||
Unit<?> getUnit() {
|
||||
return unit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal.nut;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* API implementation handling communicating with the NUT server.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NutApi {
|
||||
|
||||
private static final String LIST_VAR = "LIST VAR %s";
|
||||
private static final String VAR = "VAR %s";
|
||||
private static final String LIST_UPS = "LIST UPS";
|
||||
private static final String UPS = "UPS ";
|
||||
private static final String GET_VAR = "GET VAR %s %s";
|
||||
|
||||
private final NutResponseReader responseReader = new NutResponseReader();
|
||||
private final NutConnector connector;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param host host
|
||||
* @param port port
|
||||
* @param username username
|
||||
* @param password password
|
||||
*/
|
||||
public NutApi(final String host, final int port, final String username, final String password) {
|
||||
this.connector = new NutConnector(host, port, username, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for unit tests to inject mock connector.
|
||||
*
|
||||
* @param connector Connector.
|
||||
*/
|
||||
NutApi(final NutConnector connector) {
|
||||
this.connector = connector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connector.
|
||||
*/
|
||||
public void close() {
|
||||
connector.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of the UPS devices available from the NUT server.
|
||||
*
|
||||
* @return List of UPS devices
|
||||
* @throws NutException Exception in case of any error related to the API.
|
||||
*/
|
||||
public Map<String, String> getUpsList() throws NutException {
|
||||
return connector.read(LIST_UPS, r -> responseReader.parseList(UPS, r));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of the variables available for the given UPS.
|
||||
*
|
||||
* @param ups UPS to get the variables for
|
||||
* @return List of variables for the given UPS
|
||||
* @throws NutException Exception in case of any error related to the API.
|
||||
*/
|
||||
public Map<String, String> getVariables(final String ups) throws NutException {
|
||||
return connector.read(String.format(LIST_VAR, ups), r -> responseReader.parseList(String.format(VAR, ups), r));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of the given nut variable for the given UPS.
|
||||
*
|
||||
* @param ups UPS to get the variables for
|
||||
* @param nut The variable to the value for
|
||||
* @return Returns the value for the given nut
|
||||
* @throws NutException Exception when the variable could not retrieved
|
||||
*/
|
||||
public String getVariable(final String ups, final String nut) throws NutException {
|
||||
return connector.read(String.format(GET_VAR, ups, nut), r -> responseReader.parseVariable(ups, nut, r));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal.nut;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Connector class manages the socket connection to the NUT server.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class NutConnector {
|
||||
|
||||
private static final String USERNAME = "USERNAME %s";
|
||||
private static final String PASSWORD = "PASSWORD %s";
|
||||
private static final String OK = "OK";
|
||||
private static final String ERR = "ERR";
|
||||
private static final int TIME_OUT_MILLISECONDS = 10_000;
|
||||
private static final int MAX_RETRIES = 3;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NutConnector.class);
|
||||
|
||||
private final String login;
|
||||
private final String password;
|
||||
private final InetSocketAddress inetSocketAddress;
|
||||
private @Nullable Socket socket;
|
||||
private @Nullable BufferedReader reader;
|
||||
private @Nullable PrintWriter writer;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param host host
|
||||
* @param port port
|
||||
* @param username username
|
||||
* @param password password
|
||||
*/
|
||||
public NutConnector(final String host, final int port, final String username, final String password) {
|
||||
this.login = username.isEmpty() ? "" : String.format(USERNAME, username);
|
||||
this.password = password.isEmpty() ? "" : String.format(PASSWORD, password);
|
||||
inetSocketAddress = new InetSocketAddress(host, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Communicates to read the data to the NUT server. It handles the connection and authentication. Sends the command
|
||||
* to the NUT server and passes the reading of the values to the readFunction argument.
|
||||
*
|
||||
* @param <R> The type of the returned data
|
||||
* @param command The command to send to the NUT server
|
||||
* @param readFunction Function called to handle the lines read from the NUT server
|
||||
* @return the data read from the NUT server
|
||||
* @throws NutException Exception thrown related to the NUT server connection and/or data.
|
||||
*/
|
||||
public synchronized <R> R read(final String command, final NutFunction<NutSupplier<String>, R> readFunction)
|
||||
throws NutException {
|
||||
int retry = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
connectIfClosed();
|
||||
final PrintWriter localWriter = writer;
|
||||
final BufferedReader localReader = reader;
|
||||
|
||||
if (localWriter == null) {
|
||||
throw new NutException("Writer closed.");
|
||||
} else {
|
||||
localWriter.println(command);
|
||||
}
|
||||
if (localReader == null) {
|
||||
throw new NutException("Reader closed.");
|
||||
} else {
|
||||
return readFunction.apply(() -> readLine(localReader));
|
||||
}
|
||||
} catch (final NutException | RuntimeException e) {
|
||||
retry++;
|
||||
close();
|
||||
if (retry < MAX_RETRIES) {
|
||||
logger.debug("Error during command retry {}:", retry, e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a Socket connection if there is no connection or if the connection is closed. Authenticates if
|
||||
* username/password is provided.
|
||||
*
|
||||
* @throws NutException Exception thrown if no connection to NUT server could be made successfully.
|
||||
*/
|
||||
public void connectIfClosed() throws NutException {
|
||||
if (socket == null || socket.isClosed() || !socket.isConnected()) {
|
||||
try {
|
||||
closeStreams();
|
||||
socket = newSocket();
|
||||
socket.connect(inetSocketAddress, TIME_OUT_MILLISECONDS);
|
||||
final BufferedReader localReader = new BufferedReader(
|
||||
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
reader = localReader;
|
||||
final PrintWriter localWriter = new PrintWriter(socket.getOutputStream(), true);
|
||||
writer = localWriter;
|
||||
writeCommand(login, localReader, localWriter);
|
||||
writeCommand(password, localReader, localWriter);
|
||||
} catch (final IOException e) {
|
||||
throw new NutException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the socket.
|
||||
*/
|
||||
public synchronized void close() {
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Closing socket failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeStreams() {
|
||||
try {
|
||||
if (reader != null && socket != null && !socket.isInputShutdown()) {
|
||||
reader.close();
|
||||
}
|
||||
if (writer != null && socket != null && !socket.isOutputShutdown()) {
|
||||
writer.close();
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Closing streams failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Protected method to be able to mock the Socket connection in unit tests.
|
||||
*
|
||||
* @return a new Socket object
|
||||
*/
|
||||
protected Socket newSocket() {
|
||||
return new Socket();
|
||||
}
|
||||
|
||||
private @Nullable String readLine(final BufferedReader reader) throws NutException {
|
||||
try {
|
||||
final String line = reader.readLine();
|
||||
|
||||
if (line != null && line.startsWith(ERR)) {
|
||||
throw new NutException(line);
|
||||
}
|
||||
return line;
|
||||
} catch (final IOException e) {
|
||||
throw new NutException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCommand(final String argument, final BufferedReader reader, final PrintWriter writer)
|
||||
throws IOException, NutException {
|
||||
if (!argument.isEmpty()) {
|
||||
writer.println(argument);
|
||||
final String result = reader.readLine();
|
||||
|
||||
logger.trace("Command result: {}", result);
|
||||
if (result == null) {
|
||||
throw new NutException("No data read after sending command");
|
||||
} else if (!result.startsWith(OK)) {
|
||||
throw new NutException(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal.nut;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception thrown in case of errors related to NUT data reading/writing.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NutException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public NutException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public NutException(final Exception e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal.nut;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Function interface that can throw a {@link NutException}.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*
|
||||
* @param <T> The type returned by the function
|
||||
* @param <R> The type of the input value of the function
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@NonNullByDefault
|
||||
public interface NutFunction<T, R> {
|
||||
|
||||
/**
|
||||
* Applies this function to the given argument.
|
||||
*
|
||||
* @param t the function argument
|
||||
* @return the function result
|
||||
*/
|
||||
@Nullable
|
||||
R apply(T t) throws NutException;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal.nut;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Util class to process NUT List results.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
final class NutResponseReader {
|
||||
|
||||
private static final String BEGIN_LIST = "BEGIN LIST %s";
|
||||
private static final String END_LIST = "END LIST %s";
|
||||
private static final Pattern LIST_ROW_RESPONSE_PATTERN = Pattern.compile("([^\"]+)\"(.+)\"$");
|
||||
private static final Pattern GET_VAR_RESPONSE_PATTERN = Pattern.compile("VAR ([^\\s]+) ([^\"]+)\"(.+)\"$");
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NutResponseReader.class);
|
||||
|
||||
/**
|
||||
* Parses a NUT returned VAR.
|
||||
*
|
||||
* @param ups The ups the variable is for
|
||||
* @param nut The name of the variable
|
||||
* @param reader The reader containing the data
|
||||
* @return variable value for given nut variable name
|
||||
* @throws NutException Exception thrown in case of read errors
|
||||
*/
|
||||
public String parseVariable(final String ups, final String nut, final NutSupplier<String> reader)
|
||||
throws NutException {
|
||||
final String line = reader.get();
|
||||
|
||||
if (line == null) {
|
||||
throw new NutException(
|
||||
String.format("Variable '%s' for ups '%s' could not be read because nothing received", nut, ups));
|
||||
}
|
||||
logger.trace("Line read:{}", line);
|
||||
final Matcher matcher = GET_VAR_RESPONSE_PATTERN.matcher(line);
|
||||
|
||||
if (matcher.find() && matcher.groupCount() == 3) {
|
||||
final String matchedUps = matcher.group(1).trim();
|
||||
final String matchedNut = matcher.group(2).trim();
|
||||
final String value = stripVariable(matcher.group(3));
|
||||
|
||||
if (!ups.equals(matchedUps)) {
|
||||
throw new NutException(
|
||||
String.format("Returned value '%s' didn't match expected ups '%s'", matchedUps, ups));
|
||||
}
|
||||
if (!nut.equals(matchedNut)) {
|
||||
throw new NutException(
|
||||
String.format("Returned value '%s' didn't match expected nut '%s'", matchedNut, nut));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
throw new NutException(String.format("Variable '%s' for ups '%s' could not be read: %s", nut, ups, line));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a NUT returned LIST.
|
||||
*
|
||||
* @param type nut data type to expect in the data
|
||||
* @param reader The reader containing the data
|
||||
* @param variables The map to store the read nut variables
|
||||
* @return Map of variable name and variable value pairs
|
||||
* @throws NutException Exception thrown in case of read errors
|
||||
*/
|
||||
public Map<String, String> parseList(final String type, final NutSupplier<String> reader) throws NutException {
|
||||
final Map<String, String> variables = new HashMap<>();
|
||||
logger.trace("Reading {}", type);
|
||||
validateBegin(type, reader);
|
||||
final int stripBeginLength = type.length() + 1;
|
||||
final String endString = String.format(END_LIST, type);
|
||||
String line = null;
|
||||
boolean endFound = false;
|
||||
|
||||
while (!endFound) {
|
||||
line = reader.get();
|
||||
if (line == null) {
|
||||
throw new NutException("Unexpected end of data while reading " + type);
|
||||
}
|
||||
logger.trace("Line read:{}", line);
|
||||
endFound = endString.equals(line);
|
||||
if (!endFound) {
|
||||
addRow(variables, line, stripBeginLength);
|
||||
}
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("List '{}' read. {} variables read", type, variables.size());
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
private void validateBegin(final String type, final NutSupplier<String> reader) throws NutException {
|
||||
final String beginString = String.format(BEGIN_LIST, type);
|
||||
String line;
|
||||
|
||||
do {
|
||||
line = reader.get();
|
||||
logger.trace("Line read:{}", line);
|
||||
if (line == null) {
|
||||
throw new NutException("Could not find the begin string pattern in the data while reading " + type);
|
||||
}
|
||||
} while (!beginString.equals(line));
|
||||
logger.trace("Begin of list '{}' found", type);
|
||||
}
|
||||
|
||||
private void addRow(final Map<String, String> map, final String row, final int offset) {
|
||||
final String substring = row.substring(offset);
|
||||
final Matcher matcher = LIST_ROW_RESPONSE_PATTERN.matcher(substring);
|
||||
|
||||
if (matcher.find() && matcher.groupCount() == 2) {
|
||||
final String nut = matcher.group(1).trim();
|
||||
final String value = stripVariable(matcher.group(2));
|
||||
|
||||
map.put(nut, value);
|
||||
logger.trace("Read nut variable '{}':{}", nut, value);
|
||||
} else {
|
||||
logger.debug("Unrecognized nut results: {}", row);
|
||||
}
|
||||
}
|
||||
|
||||
private String stripVariable(final String rawVariable) {
|
||||
return rawVariable.replaceAll("\\\\\"", "\"").replaceAll("\\\\\\\\", "\\\\").trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal.nut;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Supplier interface that can throw a {@link NutException}.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*
|
||||
* @param <T> The type returned by the supplier
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@NonNullByDefault
|
||||
public interface NutSupplier<T> {
|
||||
|
||||
/**
|
||||
* Gets a result.
|
||||
*
|
||||
* @return a result
|
||||
*/
|
||||
@Nullable
|
||||
T get() throws NutException;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="networkupstools" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Network UPS Tools Binding</name>
|
||||
<description>Binding for connecting to Network UPS Tools (NUT) servers</description>
|
||||
<author>Hilbrand Bouwkamp</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:ups:config">
|
||||
<parameter name="device" type="text" required="true">
|
||||
<label>Device</label>
|
||||
<description>UPS server name</description>
|
||||
</parameter>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
<label>Host</label>
|
||||
<description>UPS server host or ip-address</description>
|
||||
<default>localhost</default>
|
||||
</parameter>
|
||||
<parameter name="username" type="text">
|
||||
<label>Username</label>
|
||||
<description>UPS server username to login</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text">
|
||||
<context>password</context>
|
||||
<label>Password</label>
|
||||
<description>UPS server password to login</description>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer">
|
||||
<label>Port</label>
|
||||
<description>UPS server port</description>
|
||||
<default>3493</default>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" unit="s">
|
||||
<label>Refresh</label>
|
||||
<description>Refresh interval for state updates in seconds</description>
|
||||
<default>60</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="channel-type:ups:dynamic-channel-config">
|
||||
<parameter name="networkupstools" type="text" required="true">
|
||||
<label>NUT Variable</label>
|
||||
<description>The name of the NUT variable</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
<config-description uri="channel-type:ups:dynamic-channel-config-quantity-type">
|
||||
<parameter name="networkupstools" type="text" required="true">
|
||||
<label>NUT Variable</label>
|
||||
<description>The name of the NUT variable</description>
|
||||
</parameter>
|
||||
<parameter name="unit" type="text" required="true">
|
||||
<label>Unit</label>
|
||||
<description>The unit of the data</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,142 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="networkupstools"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<channel-type id="ups-alarm">
|
||||
<item-type>String</item-type>
|
||||
<label>UPS Alarm</label>
|
||||
<description>UPS alarms</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="ups-load" advanced="true">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>UPS Load</label>
|
||||
<description>Load on UPS (percent)</description>
|
||||
<state pattern="%.1f %%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="ups-power" advanced="true">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>UPS Power</label>
|
||||
<description>Current value of apparent power (Volt-Amps)</description>
|
||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="ups-realpower">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>UPS Realpower</label>
|
||||
<description>Current value of real power (Watts)</description>
|
||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="ups-status">
|
||||
<item-type>String</item-type>
|
||||
<label>UPS Status</label>
|
||||
<description>Status of the UPS: OFF, OL,OB,LB,RB,OVER,TRIM,BOOST,CAL,BYPASS,NULL</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="OFF">Off</option>
|
||||
<option value="OL">Online</option>
|
||||
<option value="OB">On battery</option>
|
||||
<option value="LB">Low battery</option>
|
||||
<option value="RB">Replace battery</option>
|
||||
<option value="OVER">Overload</option>
|
||||
<option value="TRIM">Voltage trim</option>
|
||||
<option value="BOOST">Voltage boost</option>
|
||||
<option value="CAL">Calibration</option>
|
||||
<option value="BYPASS">Bypass</option>
|
||||
<option value="NULL">Null</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="ups-temperature" advanced="true">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>UPS Temperature</label>
|
||||
<description>UPS temperature (degrees C)</description>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="ups-test-result" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>UPS Test Result</label>
|
||||
<description>Results of last self test (opaque string)</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-current" advanced="true">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>Input Current</label>
|
||||
<description>Input current (A)</description>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-current-status" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Input Current Status</label>
|
||||
<description>Status relative to the thresholds</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-load">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Input Load</label>
|
||||
<description>Load on (ePDU) input (percent of full)</description>
|
||||
<state pattern="%.1f %%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-realpower" advanced="true">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Input Realpower</label>
|
||||
<description>Current sum value of all (ePDU) phases real power (W)</description>
|
||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-quality" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Input Quality</label>
|
||||
<description>Input power quality (*** opaque)</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-transfer-reason" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Input Transfer Reason</label>
|
||||
<description>Reason for last transfer to battery (*** opaque)</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-voltage" advanced="true">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>Input Voltage</label>
|
||||
<description>Input voltage (V)</description>
|
||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="input-voltage-status" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Input Voltage Status</label>
|
||||
<description>Status relative to the thresholds</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="output-current" advanced="true">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>Output Current</label>
|
||||
<description>Output current (A)</description>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="output-voltage" advanced="true">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>Output Voltage</label>
|
||||
<description>Output voltage (V)</description>
|
||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="battery-charge">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Battery Charge</label>
|
||||
<description>Battery charge (percent)</description>
|
||||
<state pattern="%.1f %%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="battery-runtime">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Battery Runtime</label>
|
||||
<description>Battery runtime (seconds)</description>
|
||||
<state pattern="%d %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="battery-voltage" advanced="true">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>Battery Voltage</label>
|
||||
<description>Battery voltage (V)</description>
|
||||
<state pattern="%.0f %unit%" readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="networkupstools"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<channel-type id="string">
|
||||
<item-type>String</item-type>
|
||||
<label>String</label>
|
||||
<description>String channel</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config"/>
|
||||
</channel-type>
|
||||
<channel-type id="number">
|
||||
<item-type>Number</item-type>
|
||||
<label>Number</label>
|
||||
<description>Number channel</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config"/>
|
||||
</channel-type>
|
||||
<channel-type id="number-electric-current">
|
||||
<item-type>Number:ElectricCurrent</item-type>
|
||||
<label>Electric Current</label>
|
||||
<description>Electric Current channel</description>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config-quantity-type"/>
|
||||
</channel-type>
|
||||
<channel-type id="number-electric-potential">
|
||||
<item-type>Number:ElectricPotential</item-type>
|
||||
<label>Electric Potential</label>
|
||||
<description>Electric Potential channel</description>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config-quantity-type"/>
|
||||
</channel-type>
|
||||
<channel-type id="number-frequency">
|
||||
<item-type>Number:Frequency</item-type>
|
||||
<label>Frequency</label>
|
||||
<description>Frequency channel</description>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config-quantity-type"/>
|
||||
</channel-type>
|
||||
<channel-type id="number-power">
|
||||
<item-type>Number:Power</item-type>
|
||||
<label>Power</label>
|
||||
<description>Power channel</description>
|
||||
<state pattern="%.1f %unit%" readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config-quantity-type"/>
|
||||
</channel-type>
|
||||
<channel-type id="number-time">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Time</label>
|
||||
<description>Time channel</description>
|
||||
<state pattern="%d %unit%" readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config-quantity-type"/>
|
||||
</channel-type>
|
||||
<channel-type id="switch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Switch</label>
|
||||
<description>Switch channel</description>
|
||||
<state readOnly="true"/>
|
||||
<config-description-ref uri="channel-type:ups:dynamic-channel-config"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="networkupstools"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="ups"
|
||||
extensible="string,number,number-electric-current,number-electric-potential,number-frequency,number-power,number-time,switch">
|
||||
<label>Network UPS Tool</label>
|
||||
<description>Network UPS Tool Thing</description>
|
||||
|
||||
<channels>
|
||||
<channel id="batteryCharge" typeId="battery-charge"/>
|
||||
<channel id="batteryRuntime" typeId="battery-runtime"/>
|
||||
<channel id="batteryVoltage" typeId="battery-voltage"/>
|
||||
<channel id="inputRealpower" typeId="input-realpower"/>
|
||||
<channel id="inputVoltageStatus" typeId="input-voltage-status"/>
|
||||
<channel id="inputQuality" typeId="input-quality"/>
|
||||
<channel id="inputCurrent" typeId="input-current"/>
|
||||
<channel id="inputCurrentStatus" typeId="input-current-status"/>
|
||||
<channel id="inputLoad" typeId="input-load"/>
|
||||
<channel id="inputTransferReason" typeId="input-transfer-reason"/>
|
||||
<channel id="inputVoltage" typeId="input-voltage"/>
|
||||
<channel id="outputCurrent" typeId="output-current"/>
|
||||
<channel id="outputVoltage" typeId="output-voltage"/>
|
||||
<channel id="upsAlarm" typeId="ups-alarm"/>
|
||||
<channel id="upsLoad" typeId="ups-load"/>
|
||||
<channel id="upsPower" typeId="ups-power"/>
|
||||
<channel id="upsRealpower" typeId="ups-realpower"/>
|
||||
<channel id="upsStatus" typeId="ups-status"/>
|
||||
<channel id="upsTemperature" typeId="ups-temperature"/>
|
||||
<channel id="upsTestResult" typeId="ups-test-result"/>
|
||||
</channels>
|
||||
|
||||
<config-description-ref uri="thing-type:ups:config"/>
|
||||
</thing-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.junit.Test;
|
||||
import org.openhab.core.library.CoreItemFactory;
|
||||
|
||||
/**
|
||||
* Test class that reads the README.md and matches it with the OH-INF thing channel definitions.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
public class NutNameChannelsTest {
|
||||
|
||||
private static final String THING_TYPES_XML = "thing-types.xml";
|
||||
private static final String CHANNELS_XML = "channels.xml";
|
||||
|
||||
private static final int EXPECTED_NUMBER_OF_CHANNELS = 20;
|
||||
private static final int EXPECTED_NUMMBER_OF_CHANNEL_XML_LINES = EXPECTED_NUMBER_OF_CHANNELS * 6;
|
||||
|
||||
// README table is: | Channel Name | Item Type | Unit | Description | Advanced
|
||||
private static final Pattern README_PATTERN = Pattern
|
||||
.compile("^\\|\\s+([\\w\\.]+)\\s+\\|\\s+([:\\w]+)\\s+\\|\\s+([^\\|]+)\\|\\s+([^\\|]+)\\|\\s+([^\\s]+)");
|
||||
private static final Pattern CHANNEL_PATTERN = Pattern.compile("<channel id");
|
||||
private static final Pattern CHANNEL_TYPE_PATTERN = Pattern
|
||||
.compile("(<channel-type|<item-type|<label|<description|<state|</channel-type)");
|
||||
|
||||
private static final String TEMPLATE_CHANNEL_TYPE = "<channel-type id=\"%s\"%s>";
|
||||
private static final String TEMPLATE_ADVANCED = " advanced=\"true\"";
|
||||
private static final String TEMPLATE_ITEM_TYPE = "<item-type>%s</item-type>";
|
||||
private static final String TEMPLATE_LABEL = "<label>%s</label>";
|
||||
private static final String TEMPLATE_DESCRIPTION = "<description>%s</description>";
|
||||
private static final String TEMPLATE_STATE = "<state pattern=\"%s\" readOnly=\"true\"/>";
|
||||
private static final String TEMPLATE_STATE_NO_PATTERN = "<state readOnly=\"true\"/>";
|
||||
private static final String TEMPLATE_STATE_OPTIONS = "<state readOnly=\"true\">";
|
||||
private static final String TEMPLATE_CHANNEL_TYPE_END = "</channel-type>";
|
||||
private static final String TEMPLATE_CHANNEL = "<channel id=\"%s\" typeId=\"%s\"/>";
|
||||
|
||||
private static final String README_IS_ADVANCED = "yes";
|
||||
|
||||
/**
|
||||
* Test if README matches with the channels in the things xml.
|
||||
*/
|
||||
@Test
|
||||
public void testReadmeMatchingChannels() {
|
||||
final Map<NutName, String> readMeNutNames = readReadme();
|
||||
final List<String> list = new ArrayList<>();
|
||||
|
||||
for (final Entry<NutName, String> entry : readMeNutNames.entrySet()) {
|
||||
final Matcher matcher = README_PATTERN.matcher(entry.getValue());
|
||||
|
||||
assertNotNull("Could not find NutName in readme for : " + entry.getValue(), entry.getKey());
|
||||
if (matcher.find()) {
|
||||
list.add(String.format(TEMPLATE_CHANNEL, entry.getKey().getChannelId(),
|
||||
nutNameToChannelType(entry.getKey())));
|
||||
} else {
|
||||
fail("Could not match line from readme: " + entry.getValue());
|
||||
}
|
||||
}
|
||||
assertThat("Expected number created channels from readme doesn't match with source code", list.size(),
|
||||
is(EXPECTED_NUMBER_OF_CHANNELS));
|
||||
final List<String> channelsFromXml = readThingsXml(CHANNEL_PATTERN, THING_TYPES_XML);
|
||||
final List<String> channelsFromReadme = list.stream().map(String::trim).sorted().collect(Collectors.toList());
|
||||
for (int i = 0; i < channelsFromXml.size(); i++) {
|
||||
assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test is the channel-type matches with the description in the README.
|
||||
* This test is a little verbose as it generates the channel-type description as in the xml is specified.
|
||||
* This is for easy adding more channels, by simply adding them to the readme and copy-paste the generated xml to
|
||||
* the channels xml.
|
||||
*/
|
||||
@Test
|
||||
public void testNutNameMatchingReadme() {
|
||||
final Map<NutName, String> readMeNutNames = readReadme();
|
||||
final List<String> list = new ArrayList<>();
|
||||
|
||||
for (final NutName nn : NutName.values()) {
|
||||
buildChannel(list, nn, readMeNutNames.get(nn));
|
||||
}
|
||||
assertThat("Expected number created channel data from readme doesn't match with source code", list.size(),
|
||||
is(EXPECTED_NUMMBER_OF_CHANNEL_XML_LINES));
|
||||
final List<String> channelsFromXml = readThingsXml(CHANNEL_TYPE_PATTERN, CHANNELS_XML);
|
||||
final List<String> channelsFromReadme = list.stream().map(String::trim).sorted().collect(Collectors.toList());
|
||||
|
||||
for (int i = 0; i < channelsFromXml.size(); i++) {
|
||||
assertThat(channelsFromXml.get(i), is(channelsFromReadme.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<NutName, String> readReadme() {
|
||||
final String path = getClass().getProtectionDomain().getClassLoader().getResource(".").getFile() + "../..";
|
||||
|
||||
try {
|
||||
final List<String> lines = FileUtils.readLines(new File(path, "README.md"));
|
||||
|
||||
return lines.stream().filter(line -> README_PATTERN.matcher(line).find())
|
||||
.collect(Collectors.toMap(this::lineToNutName, Function.identity()));
|
||||
} catch (final IOException e) {
|
||||
fail("Could not read README.md from: " + path);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> readThingsXml(final Pattern pattern, final String filename) {
|
||||
final String path = getClass().getProtectionDomain().getClassLoader().getResource(".").getFile()
|
||||
+ "../../src/main/resources/OH-INF/thing";
|
||||
try {
|
||||
final List<String> lines = FileUtils.readLines(new File(path, filename));
|
||||
return lines.stream().filter(line -> pattern.matcher(line).find()).map(String::trim).sorted()
|
||||
.collect(Collectors.toList());
|
||||
} catch (final IOException e) {
|
||||
fail("Could not read things xml from: " + path);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private NutName lineToNutName(final String line) {
|
||||
final Matcher matcher = README_PATTERN.matcher(line);
|
||||
assertTrue("Could not match readme line: " + line, matcher.find());
|
||||
final String name = matcher.group(1);
|
||||
final NutName channelIdToNutName = NutName.channelIdToNutName(name);
|
||||
assertNotNull("Name should not match null: '" + name + "' ->" + line, channelIdToNutName);
|
||||
return channelIdToNutName;
|
||||
}
|
||||
|
||||
private void buildChannel(final List<String> list, final NutName nn, final String readmeLine) {
|
||||
if (readmeLine == null) {
|
||||
fail("Readme line is null for: " + nn);
|
||||
} else {
|
||||
final Matcher matcher = README_PATTERN.matcher(readmeLine);
|
||||
|
||||
if (matcher.find()) {
|
||||
final String advanced = README_IS_ADVANCED.equals(matcher.group(5)) ? TEMPLATE_ADVANCED : "";
|
||||
|
||||
list.add(String.format(TEMPLATE_CHANNEL_TYPE, nutNameToChannelType(nn), advanced));
|
||||
final String itemType = matcher.group(2);
|
||||
|
||||
list.add(String.format(TEMPLATE_ITEM_TYPE, itemType));
|
||||
list.add(String.format(TEMPLATE_LABEL, nutNameToLabel(nn)));
|
||||
list.add(String.format(TEMPLATE_DESCRIPTION, matcher.group(4).trim()));
|
||||
final String pattern = nutNameToPattern(itemType);
|
||||
|
||||
list.add(pattern.isEmpty()
|
||||
? NutName.UPS_STATUS == nn ? TEMPLATE_STATE_OPTIONS : TEMPLATE_STATE_NO_PATTERN
|
||||
: String.format(TEMPLATE_STATE, pattern));
|
||||
} else {
|
||||
fail("Could not parse the line from README:" + readmeLine);
|
||||
}
|
||||
list.add(TEMPLATE_CHANNEL_TYPE_END);
|
||||
}
|
||||
}
|
||||
|
||||
private String nutNameToLabel(final NutName nn) {
|
||||
final String[] labelWords = nn.getName().replace("ups", "UPS").split("\\.");
|
||||
return Stream.of(labelWords).map(w -> Character.toUpperCase(w.charAt(0)) + w.substring(1))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
private String nutNameToChannelType(final NutName nn) {
|
||||
return nn.getName().replace('.', '-');
|
||||
}
|
||||
|
||||
private String nutNameToPattern(final String itemType) {
|
||||
final String pattern;
|
||||
switch (itemType) {
|
||||
case CoreItemFactory.STRING:
|
||||
pattern = "";
|
||||
break;
|
||||
case CoreItemFactory.NUMBER:
|
||||
pattern = "%d";
|
||||
break;
|
||||
case "Number:Dimensionless":
|
||||
pattern = "%.1f %%";
|
||||
break;
|
||||
case "Number:Time":
|
||||
pattern = "%d %unit%";
|
||||
break;
|
||||
case "Number:Power":
|
||||
case "Number:ElectricPotential":
|
||||
pattern = "%.0f %unit%";
|
||||
break;
|
||||
case "Number:Temperature":
|
||||
case "Number:ElectricCurrent":
|
||||
case "Number:Frequency":
|
||||
case "Number:Angle":
|
||||
pattern = "%.1f %unit%";
|
||||
break;
|
||||
default:
|
||||
fail("itemType not supported:" + itemType);
|
||||
pattern = "";
|
||||
break;
|
||||
}
|
||||
return pattern;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* Test class to check the validity of the {@link NutName} enum.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
public class NutNameTest {
|
||||
|
||||
private static final Pattern CHANNEL_PATTERN = Pattern.compile("(\\w+)\\.(\\w+)\\.?(\\w+)?\\.?(\\w+)?");
|
||||
|
||||
/**
|
||||
* Tests if the name in {@link NutName} enum matches with the channelID in the enum.
|
||||
*/
|
||||
@Test
|
||||
public void testChannelIdName() {
|
||||
for (final NutName nn : NutName.values()) {
|
||||
final Matcher matcher = CHANNEL_PATTERN.matcher(nn.getName());
|
||||
|
||||
assertTrue("NutName name '" + nn + "' could not be matched with expected pattern.", matcher.find());
|
||||
final String expectedChannelId = matcher.group(1)
|
||||
+ StringUtils.capitalize(Optional.ofNullable(matcher.group(2)).orElse(""))
|
||||
+ StringUtils.capitalize(Optional.ofNullable(matcher.group(3)).orElse(""))
|
||||
+ StringUtils.capitalize(Optional.ofNullable(matcher.group(4)).orElse(""));
|
||||
assertEquals("Channel name not correct", expectedChannelId, nn.getChannelId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.networkupstools.internal.nut;
|
||||
|
||||
import static org.hamcrest.core.Is.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
|
||||
/**
|
||||
* Unit test to test the {@link NutApi} using a mock Socket connection.
|
||||
*
|
||||
* @author Hilbrand Bouwkamp - Initial contribution
|
||||
*/
|
||||
public class NutApiTest {
|
||||
|
||||
@Mock
|
||||
private Socket socket;
|
||||
private NutConnector connector;
|
||||
|
||||
@Before
|
||||
public void setUp() throws IOException {
|
||||
initMocks(this);
|
||||
connector = new NutConnector("localhost", 0, "test", "pwd") {
|
||||
@Override
|
||||
protected Socket newSocket() {
|
||||
return socket;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if retrieving a list of variables is correctly done.
|
||||
*/
|
||||
@Test
|
||||
public void testListVariables() throws IOException, NutException, URISyntaxException {
|
||||
final String expectedCommands = new String(
|
||||
Files.readAllBytes(Paths.get(getClass().getResource("var_list_commands.txt").toURI())));
|
||||
final StringBuffer actualCommands = new StringBuffer();
|
||||
try (InputStream in = getClass().getResourceAsStream("var_list.txt"); OutputStream out = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
actualCommands.append((char) b);
|
||||
}
|
||||
}) {
|
||||
doReturn(in).when(socket).getInputStream();
|
||||
doReturn(out).when(socket).getOutputStream();
|
||||
|
||||
final NutApi api = new NutApi(connector);
|
||||
final Map<@NonNull String, @NonNull String> variables = api.getVariables("ups1");
|
||||
|
||||
assertThat("Should have variables", variables.size(), is(4));
|
||||
assertThat("Should read variable correctly", variables.get("output.voltage.nominal"), is("115"));
|
||||
assertThat("Should send commands correctly", actualCommands.toString(), is(expectedCommands));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if retrieving a single variable is correctly done.
|
||||
*/
|
||||
@Test
|
||||
public void testGetVariable() throws IOException, NutException, URISyntaxException {
|
||||
final String expectedCommands = new String(
|
||||
Files.readAllBytes(Paths.get(getClass().getResource("var_get_commands.txt").toURI())));
|
||||
final StringBuffer actualCommands = new StringBuffer();
|
||||
try (InputStream in = getClass().getResourceAsStream("var_get.txt"); OutputStream out = new OutputStream() {
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
actualCommands.append((char) b);
|
||||
}
|
||||
}) {
|
||||
doReturn(in).when(socket).getInputStream();
|
||||
doReturn(out).when(socket).getOutputStream();
|
||||
|
||||
final NutApi api = new NutApi(connector);
|
||||
final String variable = api.getVariable("ups1", "ups.status");
|
||||
assertThat("Should read ups.status variable correctly", variable, is("OL"));
|
||||
assertThat("Should send commands correctly", actualCommands.toString(), is(expectedCommands));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
OK
|
||||
OK
|
||||
VAR ups1 ups.status "OL"
|
||||
@@ -0,0 +1,3 @@
|
||||
USERNAME test
|
||||
PASSWORD pwd
|
||||
GET VAR ups1 ups.status
|
||||
@@ -0,0 +1,11 @@
|
||||
OK
|
||||
OK
|
||||
353fasa24
|
||||
|
||||
BEGIN LIST VAR ups1
|
||||
VAR ups1 ups.mfr "APC\\\""
|
||||
VAR ups1 ups.mfr.date "01/01/99"
|
||||
VAR ups1 output.voltage.nominal "115"
|
||||
VAR ups1 ups.delay.shutdown "020"
|
||||
END LIST VAR ups1
|
||||
OK
|
||||
@@ -0,0 +1,3 @@
|
||||
USERNAME test
|
||||
PASSWORD pwd
|
||||
LIST VAR ups1
|
||||
Reference in New Issue
Block a user