added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

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

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

View 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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
USERNAME test
PASSWORD pwd
GET VAR ups1 ups.status

View File

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