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,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.modbus.sunspec-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>file:${basedirRoot}/bundles/org.openhab.io.transport.modbus/target/feature/feature.xml</repository>
<feature name="openhab-binding-modbus-sunspec" description="Modbus Binding SunSpec" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-modbus</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.modbus/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.sunspec/${project.version}</bundle>
</feature>
</features>

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.modbus.sunspec.internal;
/**
* Possible values for an inverter's status field
*
* @author Nagy Attila Gábor - Initial contribution
*/
public enum InverterStatus {
OFF(1),
SLEEP(2),
ON(4),
UNKNOWN(-1);
private final int code;
InverterStatus(int code) {
this.code = code;
}
public int code() {
return this.code;
}
public static InverterStatus getByCode(int code) {
switch (code) {
case 1:
return InverterStatus.OFF;
case 2:
return InverterStatus.SLEEP;
case 4:
return InverterStatus.ON;
default:
return InverterStatus.UNKNOWN;
}
}
}

View File

@@ -0,0 +1,48 @@
/**
* 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.modbus.sunspec.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SunSpecConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Nagy Attila Gábor - Initial contribution
*/
@NonNullByDefault
public class SunSpecConfiguration {
/**
* Refresh interval in seconds
*/
public long refresh = 60;
public int maxTries = 3;// backwards compatibility and tests
/**
* Base address of the block to parse. Only used at manual setup
*/
public int address;
/**
* Length of the block to parse. Only used at manual setup
*/
public int length;
/**
* Gets refresh period in milliseconds
*/
public long getRefreshMillis() {
return refresh * 1000;
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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.modbus.sunspec.internal;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.ModbusBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SunSpecConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Nagy Attila Gábor - Initial contribution
*/
@NonNullByDefault
public class SunSpecConstants {
private static final String BINDING_ID = ModbusBindingConstants.BINDING_ID;
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_INVERTER_SINGLE_PHASE = new ThingTypeUID(BINDING_ID,
"inverter-single-phase");
public static final ThingTypeUID THING_TYPE_INVERTER_SPLIT_PHASE = new ThingTypeUID(BINDING_ID,
"inverter-split-phase");
public static final ThingTypeUID THING_TYPE_INVERTER_THREE_PHASE = new ThingTypeUID(BINDING_ID,
"inverter-three-phase");
public static final ThingTypeUID THING_TYPE_METER_SINGLE_PHASE = new ThingTypeUID(BINDING_ID, "meter-single-phase");
public static final ThingTypeUID THING_TYPE_METER_SPLIT_PHASE = new ThingTypeUID(BINDING_ID, "meter-split-phase");
public static final ThingTypeUID THING_TYPE_METER_WYE_PHASE = new ThingTypeUID(BINDING_ID, "meter-wye-phase");
public static final ThingTypeUID THING_TYPE_METER_DELTA_PHASE = new ThingTypeUID(BINDING_ID, "meter-delta-phase");
// Block types
public static final int COMMON_BLOCK = 1;
public static final int INVERTER_SINGLE_PHASE = 101;
public static final int INVERTER_SPLIT_PHASE = 102;
public static final int INVERTER_THREE_PHASE = 103;
public static final int METER_SINGLE_PHASE = 201;
public static final int METER_SPLIT_PHASE = 202;
public static final int METER_WYE_PHASE = 203;
public static final int METER_DELTA_PHASE = 204;
public static final int FINAL_BLOCK = 0xffff;
/**
* Map of the supported thing type uids, with their block type id
*/
public static final Map<Integer, ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashMap<>();
static {
SUPPORTED_THING_TYPES_UIDS.put(INVERTER_SINGLE_PHASE, THING_TYPE_INVERTER_SINGLE_PHASE);
SUPPORTED_THING_TYPES_UIDS.put(INVERTER_SPLIT_PHASE, THING_TYPE_INVERTER_SPLIT_PHASE);
SUPPORTED_THING_TYPES_UIDS.put(INVERTER_THREE_PHASE, THING_TYPE_INVERTER_THREE_PHASE);
SUPPORTED_THING_TYPES_UIDS.put(METER_SINGLE_PHASE, THING_TYPE_METER_SINGLE_PHASE);
SUPPORTED_THING_TYPES_UIDS.put(METER_SPLIT_PHASE, THING_TYPE_METER_SPLIT_PHASE);
SUPPORTED_THING_TYPES_UIDS.put(METER_WYE_PHASE, THING_TYPE_METER_WYE_PHASE);
SUPPORTED_THING_TYPES_UIDS.put(METER_DELTA_PHASE, THING_TYPE_METER_DELTA_PHASE);
}
// properties
public static final String PROPERTY_VENDOR = "vendor";
public static final String PROPERTY_MODEL = "model";
public static final String PROPERTY_VERSION = "version";
public static final String PROPERTY_PHASE_COUNT = "phaseCount";
public static final String PROPERTY_SERIAL_NUMBER = "serialNumber";
public static final String PROPERTY_BLOCK_ADDRESS = "blockAddress";
public static final String PROPERTY_BLOCK_LENGTH = "blockLength";
public static final String PROPERTY_UNIQUE_ADDRESS = "uniqueAddress";
// Channel group ids
public static final String GROUP_DEVICE_INFO = "deviceInformation";
public static final String GROUP_AC_GENERAL = "acGeneral";
public static final String GROUP_AC_PHASE_A = "acPhaseA";
public static final String GROUP_AC_PHASE_B = "acPhaseB";
public static final String GROUP_AC_PHASE_C = "acPhaseC";
public static final String GROUP_DC_GENERAL = "dcGeneral";
// List of all Channel ids in device information group
public static final String CHANNEL_PHASE_CONFIGURATION = "phase-configuration";
public static final String CHANNEL_CABINET_TEMPERATURE = "cabinet-temperature";
public static final String CHANNEL_HEATSINK_TEMPERATURE = "heatsink-temperature";
public static final String CHANNEL_TRANSFORMER_TEMPERATURE = "transformer-temperature";
public static final String CHANNEL_OTHER_TEMPERATURE = "other-temperature";
public static final String CHANNEL_STATUS = "status";
// List of channel ids in AC general group for inverter
public static final String CHANNEL_AC_TOTAL_CURRENT = "ac-total-current";
public static final String CHANNEL_AC_POWER = "ac-power";
public static final String CHANNEL_AC_FREQUENCY = "ac-frequency";
public static final String CHANNEL_AC_APPARENT_POWER = "ac-apparent-power";
public static final String CHANNEL_AC_REACTIVE_POWER = "ac-reactive-power";
public static final String CHANNEL_AC_POWER_FACTOR = "ac-power-factor";
public static final String CHANNEL_AC_LIFETIME_ENERGY = "ac-lifetime-energy";
// List of channels ids in AC general group for meter
public static final String CHANNEL_AC_AVERAGE_VOLTAGE_TO_N = "ac-average-voltage-to-n";
public static final String CHANNEL_AC_AVERAGE_VOLTAGE_TO_NEXT = "ac-average-voltage-to-next";
public static final String CHANNEL_AC_TOTAL_REAL_POWER = "ac-total-real-power";
public static final String CHANNEL_AC_TOTAL_APPARENT_POWER = "ac-total-apparent-power";
public static final String CHANNEL_AC_TOTAL_REACTIVE_POWER = "ac-total-reactive-power";
public static final String CHANNEL_AC_AVERAGE_POWER_FACTOR = "ac-average-power-factor";
public static final String CHANNEL_AC_TOTAL_EXPORTED_REAL_ENERGY = "ac-total-exported-real-energy";
public static final String CHANNEL_AC_TOTAL_IMPORTED_REAL_ENERGY = "ac-total-imported-real-energy";
public static final String CHANNEL_AC_TOTAL_EXPORTED_APPARENT_ENERGY = "ac-total-exported-apparent-energy";
public static final String CHANNEL_AC_TOTAL_IMPORTED_APPARENT_ENERGY = "ac-total-imported-apparent-energy";
public static final String CHANNEL_AC_TOTAL_IMPORTED_REACTIVE_ENERGY_Q1 = "ac-total-imported-reactive-energy-q1";
public static final String CHANNEL_AC_TOTAL_IMPORTED_REACTIVE_ENERGY_Q2 = "ac-total-imported-reactive-energy-q2";
public static final String CHANNEL_AC_TOTAL_EXPORTED_REACTIVE_ENERGY_Q3 = "ac-total-exported-reactive-energy-q3";
public static final String CHANNEL_AC_TOTAL_EXPORTED_REACTIVE_ENERGY_Q4 = "ac-total-exported-reactive-energy-q4";
// List of channel ids in AC phase group for inverter
public static final String CHANNEL_AC_PHASE_CURRENT = "ac-phase-current";
public static final String CHANNEL_AC_VOLTAGE_TO_NEXT = "ac-voltage-to-next";
public static final String CHANNEL_AC_VOLTAGE_TO_N = "ac-voltage-to-n";
// List of channel ids in DC group for inverter
public static final String CHANNEL_DC_CURRENT = "dc-current";
public static final String CHANNEL_DC_VOLTAGE = "dc-voltage";
public static final String CHANNEL_DC_POWER = "dc-power";
// List of channel ids in AC phase group for meter
public static final String CHANNEL_AC_REAL_POWER = "ac-real-power";
public static final String CHANNEL_AC_EXPORTED_REAL_ENERGY = "ac-exported-real-energy";
public static final String CHANNEL_AC_IMPORTED_REAL_ENERGY = "ac-imported-real-energy";
public static final String CHANNEL_AC_EXPORTED_APPARENT_ENERGY = "ac-exported-apparent-energy";
public static final String CHANNEL_AC_IMPORTED_APPARENT_ENERGY = "ac-imported-apparent-energy";
public static final String CHANNEL_AC_IMPORTED_REACTIVE_ENERGY_Q1 = "ac-imported-reactive-energy-q1";
public static final String CHANNEL_AC_IMPORTED_REACTIVE_ENERGY_Q2 = "ac-imported-reactive-energy-q2";
public static final String CHANNEL_AC_EXPORTED_REACTIVE_ENERGY_Q3 = "ac-exported-reactive-energy-q3";
public static final String CHANNEL_AC_EXPORTED_REACTIVE_ENERGY_Q4 = "ac-exported-reactive-energy-q4";
// Expected SunSpec ID This is a magic constant to distinguish SunSpec compatible
// devices from other modbus devices
public static final long SUNSPEC_ID = 0x53756e53;
// Size of SunSpect ID in words
public static final int SUNSPEC_ID_SIZE = 2;
// Size of any block header in words
public static final int MODEL_HEADER_SIZE = 2;
}

View File

@@ -0,0 +1,68 @@
/**
* 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.modbus.sunspec.internal;
import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.sunspec.internal.handler.InverterHandler;
import org.openhab.binding.modbus.sunspec.internal.handler.MeterHandler;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SunSpecHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Nagy Attila Gábor - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.sunspec", service = ThingHandlerFactory.class)
public class SunSpecHandlerFactory extends BaseThingHandlerFactory {
/**
* Logger instance
*/
private final Logger logger = LoggerFactory.getLogger(SunSpecHandlerFactory.class);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.containsValue(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(THING_TYPE_INVERTER_SINGLE_PHASE)
|| thingTypeUID.equals(THING_TYPE_INVERTER_SPLIT_PHASE)
|| thingTypeUID.equals(THING_TYPE_INVERTER_THREE_PHASE)) {
logger.debug("New InverterHandler created");
return new InverterHandler(thing);
} else if (thingTypeUID.equals(THING_TYPE_METER_SINGLE_PHASE)
|| thingTypeUID.equals(THING_TYPE_METER_SPLIT_PHASE) || thingTypeUID.equals(THING_TYPE_METER_WYE_PHASE)
|| thingTypeUID.equals(THING_TYPE_METER_DELTA_PHASE)) {
logger.debug("New MeterHandler created");
return new MeterHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,57 @@
/**
* 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.modbus.sunspec.internal.discovery;
import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.SUPPORTED_THING_TYPES_UIDS;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.discovery.ModbusDiscoveryListener;
import org.openhab.binding.modbus.discovery.ModbusDiscoveryParticipant;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
import org.openhab.core.thing.ThingTypeUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovery service for sunspec
*
* @author Nagy Attila Gabor - initial contribution
*
*/
@Component(immediate = true)
@NonNullByDefault
public class SunspecDiscoveryParticipant implements ModbusDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(SunspecDiscoveryParticipant.class);
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return new HashSet<ThingTypeUID>(SUPPORTED_THING_TYPES_UIDS.values());
}
@Override
public void startDiscovery(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener) {
logger.trace("Starting sunspec discovery");
try {
new SunspecDiscoveryProcess(handler, listener).detectModel();
} catch (EndpointNotInitializedException ex) {
logger.debug("Could not start discovery process");
listener.discoveryFinished();
}
}
}

View File

@@ -0,0 +1,336 @@
/**
* 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.modbus.sunspec.internal.discovery;
import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.discovery.ModbusDiscoveryListener;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
import org.openhab.binding.modbus.sunspec.internal.dto.CommonModelBlock;
import org.openhab.binding.modbus.sunspec.internal.dto.ModelBlock;
import org.openhab.binding.modbus.sunspec.internal.parser.CommonModelParser;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.thing.ThingUID;
import org.openhab.io.transport.modbus.AsyncModbusFailure;
import org.openhab.io.transport.modbus.ModbusBitUtilities;
import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
import org.openhab.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
import org.openhab.io.transport.modbus.exception.ModbusSlaveErrorResponseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class is used by the SunspecDiscoveryParticipant to detect
* the model blocks defined by the given device.
* It scans trough the defined model items and notifies the
* discovery service about the discovered devices
*
* @author Nagy Attila Gabor - Initial contribution
*/
@NonNullByDefault
public class SunspecDiscoveryProcess {
/**
* Logger instance
*/
private final Logger logger = LoggerFactory.getLogger(SunspecDiscoveryProcess.class);
/**
* The handler instance for this device
*/
private final ModbusEndpointThingHandler handler;
/**
* Listener for the discovered devices. We get this
* from the main discovery service, and it is used to
* submit any discovered Sunspec devices
*/
private final ModbusDiscoveryListener listener;
/**
* The endpoint's slave id
*/
private int slaveId;
/**
* Number of maximum retries
*/
private static final int maxTries = 3;
/**
* List of start addresses to try
*/
private Queue<Integer> possibleAddresses;
/**
* This is the base address where the next block should be searched for
*/
private int baseAddress = 40000;
/**
* Count of valid Sunspec blocks found
*/
private int blocksFound = 0;
/**
* Parser for commonblock
*/
private final CommonModelParser commonBlockParser;
/**
* The last common block found. This is used
* to get the details of any found devices
*/
private @Nullable CommonModelBlock lastCommonBlock = null;
/**
* Communication interface to the endpoint
*/
private ModbusCommunicationInterface comms;
/**
* New instances of this class should get a reference to the handler
*
* @throws EndpointNotInitializedException
*/
public SunspecDiscoveryProcess(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener)
throws EndpointNotInitializedException {
this.handler = handler;
ModbusCommunicationInterface localComms = handler.getCommunicationInterface();
if (localComms != null) {
this.comms = localComms;
} else {
throw new EndpointNotInitializedException();
}
slaveId = handler.getSlaveId();
this.listener = listener;
commonBlockParser = new CommonModelParser();
possibleAddresses = new ConcurrentLinkedQueue<>();
// Preferred and alternate base registers
// @see SunSpec Information Model Overview
possibleAddresses.add(40000);
possibleAddresses.add(50000);
possibleAddresses.add(0);
}
/**
* Start model detection
*
* @param uid the thing type to look for
* @throws EndpointNotInitializedException
*/
public void detectModel() {
if (possibleAddresses.isEmpty()) {
parsingFinished();
return;
}
// Try the next address from the possibles
baseAddress = possibleAddresses.poll();
logger.trace("Beginning scan for SunSpec device at address {}", baseAddress);
ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address
SUNSPEC_ID_SIZE, // number or words to return
maxTries);
comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::headerReceived),
this::handleError);
}
/**
* We received the first two words, that should equal to SunS
*/
private void headerReceived(ModbusRegisterArray registers) {
logger.trace("Received response from device {}", registers.toString());
Optional<DecimalType> id = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.UINT32);
if (!id.isPresent() || id.get().longValue() != SUNSPEC_ID) {
logger.debug("Could not find SunSpec DID at address {}, received: {}, expected: {}", baseAddress, id,
SUNSPEC_ID);
detectModel();
return;
}
logger.trace("Header looks correct");
baseAddress += SUNSPEC_ID_SIZE;
lookForModelBlock();
}
/**
* Look for a valid model block at the current base address
*/
private void lookForModelBlock() {
ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, baseAddress, // Start address
MODEL_HEADER_SIZE, // number or words to return
maxTries);
comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::modelBlockReceived),
this::handleError);
}
/**
* We received a model block header
*/
private void modelBlockReceived(ModbusRegisterArray registers) {
logger.debug("Received response from device {}", registers.toString());
Optional<DecimalType> moduleID = ModbusBitUtilities.extractStateFromRegisters(registers, 0, ValueType.UINT16);
Optional<DecimalType> blockLength = ModbusBitUtilities.extractStateFromRegisters(registers, 1,
ValueType.UINT16);
if (!moduleID.isPresent() || !blockLength.isPresent()) {
logger.info("Could not find valid module id or block length field.");
parsingFinished();
return;
}
ModelBlock block = new ModelBlock();
block.address = baseAddress;
block.moduleID = moduleID.get().intValue();
block.length = blockLength.get().intValue() + MODEL_HEADER_SIZE;
logger.debug("SunSpec detector found block {}", block);
blocksFound++;
if (block.moduleID == FINAL_BLOCK) {
parsingFinished();
} else {
baseAddress += block.length;
if (block.moduleID == COMMON_BLOCK) {
readCommonBlock(block); // This is an asynchronous task
return;
} else {
createDiscoveryResult(block);
lookForModelBlock();
}
}
}
/**
* Start reading common block
*
* @param block
*/
private void readCommonBlock(ModelBlock block) {
ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(slaveId,
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, block.address, // Start address
block.length, // number or words to return
maxTries);
comms.submitOneTimePoll(request, result -> result.getRegisters().ifPresent(this::parseCommonBlock),
this::handleError);
}
/**
* We've read the details of a common block now parse it, and
* store for later use
*
* @param registers
*/
private void parseCommonBlock(ModbusRegisterArray registers) {
logger.trace("Got common block data: {}", registers);
lastCommonBlock = commonBlockParser.parse(registers);
lookForModelBlock(); // Continue parsing
}
/**
* Create a discovery result from a model block
*
* @param block the block we've found
*/
private void createDiscoveryResult(ModelBlock block) {
if (!SUPPORTED_THING_TYPES_UIDS.containsKey(block.moduleID)) {
logger.debug("ModuleID {} is not supported, skipping this block", block.moduleID);
return;
}
CommonModelBlock commonBlock = lastCommonBlock;
if (commonBlock == null) {
logger.warn(
"Found model block without a preceding common block. Can't add device because details are unkown");
return;
}
ThingUID thingUID = new ThingUID(SUPPORTED_THING_TYPES_UIDS.get(block.moduleID), handler.getUID(),
Integer.toString(block.address));
Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_VENDOR, commonBlock.manufacturer);
properties.put(PROPERTY_MODEL, commonBlock.model);
properties.put(PROPERTY_SERIAL_NUMBER, commonBlock.serialNumber);
properties.put(PROPERTY_VERSION, commonBlock.version);
properties.put(PROPERTY_BLOCK_ADDRESS, block.address);
properties.put(PROPERTY_BLOCK_LENGTH, block.length);
properties.put(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(PROPERTY_UNIQUE_ADDRESS).withBridge(handler.getUID())
.withLabel(commonBlock.manufacturer + " " + commonBlock.model).build();
listener.thingDiscovered(result);
}
/**
* Parsing of model blocks finished
* Now we have to report back to the handler the common block and the block we were looking for
*/
private void parsingFinished() {
listener.discoveryFinished();
}
/**
* Handle errors received during communication
*/
private void handleError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
if (blocksFound > 1 && failure.getCause() instanceof ModbusSlaveErrorResponseException) {
int code = ((ModbusSlaveErrorResponseException) failure.getCause()).getExceptionCode();
if (code == ModbusSlaveErrorResponseException.ILLEGAL_DATA_ACCESS
|| code == ModbusSlaveErrorResponseException.ILLEGAL_DATA_VALUE) {
// It is very likely that the slave does not report an end block (0xffff) after the main blocks
// so we treat this situation as normal.
logger.debug(
"Seems like slave device does not report an end block. Continuing with the dectected blocks");
parsingFinished();
return;
}
}
String cls = failure.getCause().getClass().getName();
String msg = failure.getCause().getMessage();
logger.warn("Error with read at address {}: {} {}", baseAddress, cls, msg);
detectModel();
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.modbus.sunspec.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This class contains information parsed from the Common Model block
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
@NonNullByDefault
public class CommonModelBlock {
/**
* Value = 0x0001. Uniquely identifies this as a SunSpec Common Model Block
*/
public int sunSpecDID = 0x0001;
/**
* Length of block in 16-bit registers
*/
public int length = 0;
/**
* Modbus unit ID - this is a unique identifier of the device
*/
public int deviceAddress = 0;
// Manufacturer specific values
public String manufacturer = "";
public String model = "";
public String version = "";
public String serialNumber = "";
}

View File

@@ -0,0 +1,219 @@
/**
* 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.modbus.sunspec.internal.dto;
import java.util.Optional;
/**
* Model for SunSpec compatible inverter data
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
public class InverterModelBlock {
/**
* Type of inverter (single phase, split phase, three phase)
*/
public Integer phaseConfiguration;
/**
* Length of the block in 16bit words
*/
public Integer length;
/**
* AC Total Current value
*/
public Integer acCurrentTotal;
/**
* AC Phase A Current value
*/
public Integer acCurrentPhaseA;
/**
* AC Phase B Current value
*/
public Optional<Integer> acCurrentPhaseB;
/**
* AC Phase C Current value
*/
public Optional<Integer> acCurrentPhaseC;
/**
* AC Current scale factor
*/
public Short acCurrentSF;
/**
* AC Voltage Phase AB value
*/
public Optional<Integer> acVoltageAB;
/**
* AC Voltage Phase BC value
*/
public Optional<Integer> acVoltageBC;
/**
* AC Voltage Phase CA value
*/
public Optional<Integer> acVoltageCA;
/**
* AC Voltage Phase A to N value
*/
public Integer acVoltageAtoN;
/**
* AC Voltage Phase B to N value
*/
public Optional<Integer> acVoltageBtoN;
/**
* AC Voltage Phase C to N value
*/
public Optional<Integer> acVoltageCtoN;
/**
* AC Voltage scale factor
*/
public Short acVoltageSF;
/**
* AC Power value
*/
public Short acPower;
/**
* AC Power scale factor
*/
public Short acPowerSF;
/**
* AC Frequency value
*/
public Integer acFrequency;
/**
* AC Frequency scale factor
*/
public Short acFrequencySF;
/**
* Apparent power
*/
public Optional<Short> acApparentPower;
/**
* Apparent power scale factor
*/
public Optional<Short> acApparentPowerSF;
/**
* Reactive power
*/
public Optional<Short> acReactivePower;
/**
* Reactive power scale factor
*/
public Optional<Short> acReactivePowerSF;
/**
* Power factor
*/
public Optional<Short> acPowerFactor;
/**
* Power factor scale factor
*/
public Optional<Short> acPowerFactorSF;
/**
* AC Lifetime Energy production
*/
public Long acEnergyLifetime;
/**
* AC Lifetime Energy scale factor
*/
public Short acEnergyLifetimeSF;
/**
* DC Current value
*/
public Optional<Integer> dcCurrent;
/**
* DC Current scale factor
*/
public Optional<Short> dcCurrentSF;
/**
* DC Voltage value
*/
public Optional<Integer> dcVoltage;
/**
* DC Voltage scale factor
*/
public Optional<Short> dcVoltageSF;
/**
* DC Power value
*/
public Optional<Short> dcPower;
/**
* DC Power scale factor
*/
public Optional<Short> dcPowerSF;
/**
* Cabinet temperature
*/
public Short temperatureCabinet;
/**
* Heat sink temperature
*/
public Optional<Short> temperatureHeatsink;
/**
* Transformer temperature
*/
public Optional<Short> temperatureTransformer;
/**
* Other temperature
*/
public Optional<Short> temperatureOther;
/**
* Heat sink temperature scale factor
*/
public Short temperatureSF;
/**
* Current operating state
*/
public Integer status;
/**
* Vendor defined operating state or error code
*/
public Optional<Integer> statusVendor;
}

View File

@@ -0,0 +1,261 @@
/**
* 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.modbus.sunspec.internal.dto;
import java.util.Optional;
/**
*
* Data object for the parsed information from a sunspec meter
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
public class MeterModelBlock {
/**
* Sunspec device type id
*/
public Integer sunspecDID;
/**
* Block length
*/
public Integer length;
/**
* AC Total Current value
*/
public Short acCurrentTotal;
/**
* Descriptors for phase A
*/
public PhaseBlock phaseA = new PhaseBlock();
/**
* Descriptors for phase B
*/
public PhaseBlock phaseB = new PhaseBlock();
/**
* Descriptors for phase C
*/
public PhaseBlock phaseC = new PhaseBlock();
/**
* AC Current scale factor
*/
public Short acCurrentSF;
/**
* AC Voltage Line to line value
*/
public Optional<Short> acVoltageLineToNAverage;
/**
* AC Voltage Line to N value
*/
public Optional<Short> acVoltageLineToLineAverage;
/**
* AC Voltage scale factor
*/
public Short acVoltageSF;
/**
* AC Frequency value
*/
public Short acFrequency;
/**
* AC Frequency scale factor
*/
public Optional<Short> acFrequencySF;
/**
* Total real power
*/
public Short acRealPowerTotal;
/**
* AC Real Power Scale Factor
*/
public Short acRealPowerSF;
/**
* Total apparent power
*/
public Optional<Short> acApparentPowerTotal;
/**
* AC Apparent Power Scale Factor
*/
public Optional<Short> acApparentPowerSF;
/**
* Total reactive power
*/
public Optional<Short> acReactivePowerTotal;
/**
* AC Reactive Power Scale Factor
*/
public Optional<Short> acReactivePowerSF;
/**
* Power factor
*/
public Optional<Short> acPowerFactor;
/**
* Power factor scale factor
*/
public Optional<Short> acPowerFactorSF;
/**
* Total exported real energy
*/
public Optional<Long> acExportedRealEnergyTotal;
/**
* Total imported real energy
*/
public Long acImportedRealEnergyTotal;
/**
* Real Energy Scale Factor
*/
public Short acRealEnergySF;
/**
* Total exported apparent energy
*/
public Optional<Long> acExportedApparentEnergyTotal;
/**
* Total imported apparent energy
*/
public Optional<Long> acImportedApparentEnergyTotal;
/**
* Apparent Energy Scale Factor
*/
public Optional<Short> acApparentEnergySF;
/**
* Quadrant 1: Total imported reactive energy
*/
public Optional<Long> acImportedReactiveEnergyQ1Total;
/**
* Quadrant 2: Total imported reactive energy
*/
public Optional<Long> acImportedReactiveEnergyQ2Total;
/**
* Quadrant 3: Total exported reactive energy
*/
public Optional<Long> acExportedReactiveEnergyQ3Total;
/**
* Quadrant 4: Total exported reactive energy
*/
public Optional<Long> acExportedReactiveEnergyQ4Total;
/**
* Reactive Energy Scale Factor
*/
public Optional<Short> acReactiveEnergySF;
/**
* This subclass is used to store raw data for a single phase in
* multi phase meters.
*/
public static class PhaseBlock {
/**
* AC Phase A Current value
*/
public Optional<Short> acPhaseCurrent;
/**
* AC Voltage Phase Phase to N value
*/
public Optional<Short> acVoltageToN;
/**
* AC Voltage Phase Line to next Line value
*/
public Optional<Short> acVoltageToNext;
/**
* Phase A AC real power
*/
public Optional<Short> acRealPower;
/**
* Phase A AC apparent power
*/
public Optional<Short> acApparentPower;
/**
* Phase A AC reactive power
*/
public Optional<Short> acReactivePower;
/**
* Phase A Power factor
*/
public Optional<Short> acPowerFactor;
/**
* Phase A exported real energy
*/
public Optional<Long> acExportedRealEnergy;
/**
* Phase A imported real energy
*/
public Optional<Long> acImportedRealEnergy;
/**
* Phase A exported apparent energy
*/
public Optional<Long> acExportedApparentEnergy;
/**
* Phase A imported apparent energy
*/
public Optional<Long> acImportedApparentEnergy;
/**
* Quadrant 1: Phase A imported reactive energy
*/
public Optional<Long> acImportedReactiveEnergyQ1;
/**
* Quadrant 2: Phase A imported reactive energy
*/
public Optional<Long> acImportedReactiveEnergyQ2;
/**
* Quadrant 3: Phase A exported reactive energy
*/
public Optional<Long> acExportedReactiveEnergyQ3;
/**
* Quadrant 4: Phase A exported reactive energy
*/
public Optional<Long> acExportedReactiveEnergyQ4;
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.modbus.sunspec.internal.dto;
/**
* Descriptor for a model block found on the device
* This DTO contains only the metadata required to
* address the block at the modbus register space
*
* @author Nagy Attila Gabor - Initial contribution
*/
public class ModelBlock {
/**
* Base address of this block in 16bit words
*/
public int address;
/**
* Length of this block in 16bit words
*/
public int length;
/**
* Module identifier
*/
public int moduleID;
@Override
public String toString() {
return String.format("ModelBlock type=%d address=%d length=%d", moduleID, address, length);
}
}

View File

@@ -0,0 +1,467 @@
/**
* 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.modbus.sunspec.internal.handler;
import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.handler.EndpointNotInitializedException;
import org.openhab.binding.modbus.handler.ModbusEndpointThingHandler;
import org.openhab.binding.modbus.sunspec.internal.SunSpecConfiguration;
import org.openhab.binding.modbus.sunspec.internal.dto.ModelBlock;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.Bridge;
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.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.io.transport.modbus.AsyncModbusFailure;
import org.openhab.io.transport.modbus.ModbusCommunicationInterface;
import org.openhab.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
import org.openhab.io.transport.modbus.PollTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AbstractSunSpecHandler} is the base class for any sunspec handlers
* Common things are handled here:
*
* - loads the configuration either from the configuration file or
* from the properties that have been set by the auto discovery
* - sets up a regular poller to the device
* - handles incoming messages from the device:
* - common properties are parsed and published
* - other values are submitted to child implementations
* - handles disposal of the device by removing any handlers
* - implements some tool methods
*
* @author Nagy Attila Gabor - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractSunSpecHandler extends BaseThingHandler {
/**
* Logger instance
*/
private final Logger logger = LoggerFactory.getLogger(AbstractSunSpecHandler.class);
/**
* Configuration instance
*/
protected @Nullable SunSpecConfiguration config = null;
/**
* This is the task used to poll the device
*/
private volatile @Nullable PollTask pollTask = null;
/**
* Communication interface to the slave endpoint we're connecting to
*/
protected volatile @Nullable ModbusCommunicationInterface comms = null;
/**
* This is the slave id, we store this once initialization is complete
*/
private volatile int slaveId;
/**
* Instances of this handler should get a reference to the modbus manager
*
* @param thing the thing to handle
* @param managerRef the modbus manager
*/
public AbstractSunSpecHandler(Thing thing) {
super(thing);
}
/**
* Handle incoming commands. This binding is read-only by default
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Currently we do not support any commands
}
/**
* Initialization:
* Load the config object of the block
* Connect to the slave bridge
* Start the periodic polling
*/
@Override
public void initialize() {
config = getConfigAs(SunSpecConfiguration.class);
logger.debug("Initializing thing with properties: {}", thing.getProperties());
startUp();
}
/*
* This method starts the operation of this handler
* Load the config object of the block
* Connect to the slave bridge
* Start the periodic polling
*/
private void startUp() {
connectEndpoint();
if (comms == null || config == null) {
logger.debug("Invalid endpoint/config/manager ref for sunspec handler");
return;
}
if (pollTask != null) {
return;
}
// Try properties first
@Nullable
ModelBlock mainBlock = getAddressFromProperties();
if (mainBlock == null) {
mainBlock = getAddressFromConfig();
}
if (mainBlock != null) {
publishUniqueAddress(mainBlock);
updateStatus(ThingStatus.UNKNOWN);
registerPollTask(mainBlock);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"SunSpec item should either have the address and length configuration set or should been created by auto discovery");
return;
}
}
/**
* Load and parse configuration from the properties
* These will be set by the auto discovery process
*/
private @Nullable ModelBlock getAddressFromProperties() {
Map<String, String> properties = thing.getProperties();
if (!properties.containsKey(PROPERTY_BLOCK_ADDRESS) || !properties.containsKey(PROPERTY_BLOCK_LENGTH)) {
return null;
}
try {
ModelBlock block = new ModelBlock();
block.address = (int) Double.parseDouble(thing.getProperties().get(PROPERTY_BLOCK_ADDRESS));
block.length = (int) Double.parseDouble(thing.getProperties().get(PROPERTY_BLOCK_LENGTH));
return block;
} catch (NumberFormatException ex) {
logger.debug("Could not parse address and length properties, error: {}", ex.getMessage());
return null;
}
}
/**
* Load configuration from main configuration
*/
private @Nullable ModelBlock getAddressFromConfig() {
@Nullable
SunSpecConfiguration myconfig = config;
if (myconfig == null) {
return null;
}
ModelBlock block = new ModelBlock();
block.address = myconfig.address;
block.length = myconfig.length;
return block;
}
/**
* Publish the unique address property if it has not been set before
*/
private void publishUniqueAddress(ModelBlock block) {
Map<String, String> properties = getThing().getProperties();
if (properties.containsKey(PROPERTY_UNIQUE_ADDRESS) && !properties.get(PROPERTY_UNIQUE_ADDRESS).isEmpty()) {
logger.debug("Current unique address is: {}", properties.get(PROPERTY_UNIQUE_ADDRESS));
return;
}
ModbusEndpointThingHandler handler = getEndpointThingHandler();
if (handler == null) {
return;
}
getThing().setProperty(PROPERTY_UNIQUE_ADDRESS, handler.getUID().getAsString() + ":" + block.address);
}
/**
* Dispose the binding correctly
*/
@Override
public void dispose() {
tearDown();
}
/**
* Unregister the poll task and release the endpoint reference
*/
private void tearDown() {
unregisterPollTask();
unregisterEndpoint();
}
/**
* Returns the current slave id from the bridge
*/
public int getSlaveId() {
return slaveId;
}
/**
* Get the endpoint handler from the bridge this handler is connected to
* Checks that we're connected to the right type of bridge
*
* @return the endpoint handler or null if the bridge does not exist
*/
private @Nullable ModbusEndpointThingHandler getEndpointThingHandler() {
Bridge bridge = getBridge();
if (bridge == null) {
logger.debug("Bridge is null");
return null;
}
if (bridge.getStatus() != ThingStatus.ONLINE) {
logger.debug("Bridge is not online");
return null;
}
ThingHandler handler = bridge.getHandler();
if (handler == null) {
logger.debug("Bridge handler is null");
return null;
}
if (handler instanceof ModbusEndpointThingHandler) {
ModbusEndpointThingHandler slaveEndpoint = (ModbusEndpointThingHandler) handler;
return slaveEndpoint;
} else {
logger.debug("Unexpected bridge handler: {}", handler);
return null;
}
}
/**
* Get a reference to the modbus endpoint
*/
private void connectEndpoint() {
if (comms != null) {
return;
}
ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
if (slaveEndpointThingHandler == null) {
@SuppressWarnings("null")
String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
String.format("Bridge '%s' is offline", label));
logger.debug("No bridge handler available -- aborting init for {}", label);
return;
}
try {
slaveId = slaveEndpointThingHandler.getSlaveId();
comms = slaveEndpointThingHandler.getCommunicationInterface();
} catch (EndpointNotInitializedException e) {
// this will be handled below as endpoint remains null
}
if (comms == null) {
@SuppressWarnings("null")
String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("<null>");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE,
String.format("Bridge '%s' not completely initialized", label));
logger.debug("Bridge not initialized fully (no endpoint) -- aborting init for {}", this);
return;
}
}
/**
* Remove the endpoint if exists
*/
private void unregisterEndpoint() {
// Comms will be close()'d by endpoint thing handler
comms = null;
}
/**
* Register poll task
* This is where we set up our regular poller
*/
private synchronized void registerPollTask(ModelBlock mainBlock) {
if (pollTask != null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
throw new IllegalStateException("pollTask should be unregistered before registering a new one!");
}
@Nullable
ModbusCommunicationInterface mycomms = comms;
@Nullable
SunSpecConfiguration myconfig = config;
if (myconfig == null || mycomms == null) {
throw new IllegalStateException("registerPollTask called without proper configuration");
}
logger.debug("Setting up regular polling");
ModbusReadRequestBlueprint request = new ModbusReadRequestBlueprint(getSlaveId(),
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
long refreshMillis = myconfig.getRefreshMillis();
pollTask = mycomms.registerRegularPoll(request, refreshMillis, 1000, result -> {
result.getRegisters().ifPresent(this::handlePolledData);
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
}
}, this::handleError);
}
/**
* This method should handle incoming poll data, and update the channels
* with the values received
*/
protected abstract void handlePolledData(ModbusRegisterArray registers);
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
super.bridgeStatusChanged(bridgeStatusInfo);
logger.debug("Thing status changed to {}", this.getThing().getStatus().name());
if (getThing().getStatus() == ThingStatus.ONLINE) {
startUp();
} else if (getThing().getStatus() == ThingStatus.OFFLINE) {
tearDown();
}
}
/**
* Unregister poll task.
*
* No-op in case no poll task is registered, or if the initialization is incomplete.
*/
private synchronized void unregisterPollTask() {
@Nullable
PollTask task = pollTask;
if (task == null) {
return;
}
logger.debug("Unregistering polling from ModbusManager");
@Nullable
ModbusCommunicationInterface mycomms = comms;
if (mycomms != null) {
mycomms.unregisterRegularPoll(task);
}
pollTask = null;
}
/**
* Handle errors received during communication
*/
protected void handleError(AsyncModbusFailure<ModbusReadRequestBlueprint> failure) {
// Ignore all incoming data and errors if configuration is not correct
if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
return;
}
String msg = failure.getCause().getMessage();
String cls = failure.getCause().getClass().getName();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Error with read: %s: %s", cls, msg));
}
/**
* Returns true, if we're in a CONFIGURATION_ERROR state
*
* @return
*/
protected boolean hasConfigurationError() {
ThingStatusInfo statusInfo = getThing().getStatusInfo();
return statusInfo.getStatus() == ThingStatus.OFFLINE
&& statusInfo.getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR;
}
/**
* Reset communication status to ONLINE if we're in an OFFLINE state
*/
protected void resetCommunicationError() {
ThingStatusInfo statusInfo = thing.getStatusInfo();
if (ThingStatus.OFFLINE.equals(statusInfo.getStatus())
&& ThingStatusDetail.COMMUNICATION_ERROR.equals(statusInfo.getStatusDetail())) {
updateStatus(ThingStatus.ONLINE);
}
}
/**
* Returns the channel UID for the specified group and channel id
*
* @param string the channel group
* @param string the channel id in that group
* @return the globally unique channel uid
*/
ChannelUID channelUID(String group, String id) {
return new ChannelUID(getThing().getUID(), group, id);
}
/**
* Returns value multiplied by the 10 on the power of scaleFactory
*
* @param value the value to alter
* @param scaleFactor the scale factor to use (may be negative)
* @return the scaled value as a DecimalType
*/
protected State getScaled(Optional<? extends Number> value, Optional<Short> scaleFactor, Unit<?> unit) {
if (!value.isPresent() || !scaleFactor.isPresent()) {
return UnDefType.UNDEF;
}
return getScaled(value.get().longValue(), scaleFactor.get(), unit);
}
/**
* Returns value multiplied by the 10 on the power of scaleFactory
*
* @param value the value to alter
* @param scaleFactor the scale factor to use (may be negative)
* @return the scaled value as a DecimalType
*/
protected State getScaled(Optional<? extends Number> value, Short scaleFactor, Unit<?> unit) {
return getScaled(value, Optional.of(scaleFactor), unit);
}
/**
* Returns value multiplied by the 10 on the power of scaleFactory
*
* @param value the value to alter
* @param scaleFactor the scale factor to use (may be negative)
* @return the scaled value as a DecimalType
*/
protected State getScaled(Number value, Short scaleFactor, Unit<?> unit) {
if (scaleFactor == 0) {
return new QuantityType<>(value.longValue(), unit);
}
return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);
}
}

View File

@@ -0,0 +1,151 @@
/**
* 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.modbus.sunspec.internal.handler;
import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.sunspec.internal.InverterStatus;
import org.openhab.binding.modbus.sunspec.internal.dto.InverterModelBlock;
import org.openhab.binding.modbus.sunspec.internal.parser.InverterModelParser;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.UnDefType;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link InverterHandler} is responsible for handling commands, which are
* sent to an inverter and publishing the received values to OpenHAB.
*
* @author Nagy Attila Gabor - Initial contribution
*/
@NonNullByDefault
public class InverterHandler extends AbstractSunSpecHandler {
/**
* Parser used to convert incoming raw messages into model blocks
*/
private final InverterModelParser parser = new InverterModelParser();
/**
* Logger instance
*/
private final Logger logger = LoggerFactory.getLogger(InverterHandler.class);
public InverterHandler(Thing thing) {
super(thing);
}
/**
* This method is called each time new data has been polled from the modbus slave
* The register array is first parsed, then each of the channels are updated
* to the new values
*
* @param registers byte array read from the modbus slave
*/
@Override
protected void handlePolledData(ModbusRegisterArray registers) {
logger.trace("Model block received, size: {}", registers.size());
InverterModelBlock block = parser.parse(registers);
// Device information group
updateState(channelUID(GROUP_DEVICE_INFO, CHANNEL_CABINET_TEMPERATURE),
getScaled(block.temperatureCabinet, block.temperatureSF, CELSIUS));
updateState(channelUID(GROUP_DEVICE_INFO, CHANNEL_HEATSINK_TEMPERATURE),
getScaled(block.temperatureHeatsink, Optional.of(block.temperatureSF), CELSIUS));
updateState(channelUID(GROUP_DEVICE_INFO, CHANNEL_TRANSFORMER_TEMPERATURE),
getScaled(block.temperatureTransformer, Optional.of(block.temperatureSF), CELSIUS));
updateState(channelUID(GROUP_DEVICE_INFO, CHANNEL_OTHER_TEMPERATURE),
getScaled(block.temperatureOther, Optional.of(block.temperatureSF), CELSIUS));
InverterStatus status = InverterStatus.getByCode(block.status);
updateState(channelUID(GROUP_DEVICE_INFO, CHANNEL_STATUS),
status == null ? UnDefType.UNDEF : new StringType(status.name()));
// AC General group
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_CURRENT),
getScaled(block.acCurrentTotal, block.acCurrentSF, AMPERE));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_POWER), getScaled(block.acPower, block.acPowerSF, WATT));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_FREQUENCY),
getScaled(block.acFrequency, block.acFrequencySF, HERTZ));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_APPARENT_POWER),
getScaled(block.acApparentPower, block.acApparentPowerSF, WATT)); // TODO: VA currently not supported,
// see:
// https://github.com/openhab/openhab-core/pull/1347
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_REACTIVE_POWER),
getScaled(block.acReactivePower, block.acReactivePowerSF, WATT)); // TODO: var currently not supported,
// see:
// https://github.com/openhab/openhab-core/pull/1347
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_POWER_FACTOR),
getScaled(block.acPowerFactor, block.acPowerFactorSF, PERCENT));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_LIFETIME_ENERGY),
getScaled(block.acEnergyLifetime, block.acEnergyLifetimeSF, WATT_HOUR));
// DC General group
updateState(channelUID(GROUP_DC_GENERAL, CHANNEL_DC_CURRENT),
getScaled(block.dcCurrent, block.dcCurrentSF, AMPERE));
updateState(channelUID(GROUP_DC_GENERAL, CHANNEL_DC_VOLTAGE),
getScaled(block.dcVoltage, block.dcVoltageSF, VOLT));
updateState(channelUID(GROUP_DC_GENERAL, CHANNEL_DC_POWER), getScaled(block.dcPower, block.dcPowerSF, WATT));
// AC Phase specific groups
// All types of inverters
updateState(channelUID(GROUP_AC_PHASE_A, CHANNEL_AC_PHASE_CURRENT),
getScaled(block.acCurrentPhaseA, block.acCurrentSF, AMPERE));
updateState(channelUID(GROUP_AC_PHASE_A, CHANNEL_AC_VOLTAGE_TO_NEXT),
getScaled(block.acVoltageAB, block.acVoltageSF, VOLT));
updateState(channelUID(GROUP_AC_PHASE_A, CHANNEL_AC_VOLTAGE_TO_N),
getScaled(block.acVoltageAtoN, block.acVoltageSF, VOLT));
// Split phase and three phase
if ((thing.getThingTypeUID().equals(THING_TYPE_INVERTER_SPLIT_PHASE)
|| thing.getThingTypeUID().equals(THING_TYPE_INVERTER_THREE_PHASE))
&& block.phaseConfiguration >= INVERTER_SPLIT_PHASE) {
updateState(channelUID(GROUP_AC_PHASE_B, CHANNEL_AC_PHASE_CURRENT),
getScaled(block.acCurrentPhaseB, block.acCurrentSF, AMPERE));
updateState(channelUID(GROUP_AC_PHASE_B, CHANNEL_AC_VOLTAGE_TO_NEXT),
getScaled(block.acVoltageBC, block.acVoltageSF, VOLT));
updateState(channelUID(GROUP_AC_PHASE_B, CHANNEL_AC_VOLTAGE_TO_N),
getScaled(block.acVoltageBtoN, block.acVoltageSF, VOLT));
}
// Three phase only
if (thing.getThingTypeUID().equals(THING_TYPE_INVERTER_THREE_PHASE)
&& block.phaseConfiguration >= INVERTER_THREE_PHASE) {
updateState(channelUID(GROUP_AC_PHASE_C, CHANNEL_AC_PHASE_CURRENT),
getScaled(block.acCurrentPhaseC, block.acCurrentSF, AMPERE));
updateState(channelUID(GROUP_AC_PHASE_C, CHANNEL_AC_VOLTAGE_TO_NEXT),
getScaled(block.acVoltageCA, block.acVoltageSF, VOLT));
updateState(channelUID(GROUP_AC_PHASE_C, CHANNEL_AC_VOLTAGE_TO_N),
getScaled(block.acVoltageCtoN, block.acVoltageSF, VOLT));
}
resetCommunicationError();
}
}

View File

@@ -0,0 +1,211 @@
/**
* 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.modbus.sunspec.internal.handler;
import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.sunspec.internal.dto.MeterModelBlock;
import org.openhab.binding.modbus.sunspec.internal.parser.MeterModelParser;
import org.openhab.core.thing.Thing;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This handler is responsible for handling data recieved from a sunspec meter
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
@NonNullByDefault
public class MeterHandler extends AbstractSunSpecHandler {
/**
* Parser used to convert incoming raw messages into model blocks
*/
private final MeterModelParser parser = new MeterModelParser();
/**
* Logger instance
*/
private final Logger logger = LoggerFactory.getLogger(MeterHandler.class);
public MeterHandler(Thing thing) {
super(thing);
}
/**
* Receive polled data, parse then update states
*/
@Override
protected void handlePolledData(ModbusRegisterArray registers) {
logger.trace("Model block received, size: {}", registers.size());
MeterModelBlock block = parser.parse(registers);
// AC General group
updateTotalValues(block);
updatePhaseValues(block, block.phaseA, GROUP_AC_PHASE_A);
// Split phase, wye/delta phase
if (block.sunspecDID >= METER_SPLIT_PHASE && (thing.getThingTypeUID().equals(THING_TYPE_METER_SPLIT_PHASE)
|| thing.getThingTypeUID().equals(THING_TYPE_METER_WYE_PHASE)
|| thing.getThingTypeUID().equals(THING_TYPE_METER_DELTA_PHASE))) {
updatePhaseValues(block, block.phaseB, GROUP_AC_PHASE_B);
}
// Three phase (wye/delta) only
if (block.sunspecDID >= INVERTER_THREE_PHASE && (thing.getThingTypeUID().equals(THING_TYPE_METER_WYE_PHASE)
|| thing.getThingTypeUID().equals(THING_TYPE_METER_DELTA_PHASE))) {
updatePhaseValues(block, block.phaseC, GROUP_AC_PHASE_C);
}
resetCommunicationError();
}
/**
* Update the total states from the received block
*
* @param block
*/
private void updateTotalValues(MeterModelBlock block) {
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_CURRENT),
getScaled(block.acCurrentTotal, block.acCurrentSF, AMPERE));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_AVERAGE_VOLTAGE_TO_N),
getScaled(block.acVoltageLineToNAverage, block.acVoltageSF, VOLT));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_AVERAGE_VOLTAGE_TO_NEXT),
getScaled(block.acVoltageLineToLineAverage, block.acVoltageSF, VOLT));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_FREQUENCY),
getScaled(block.acFrequency, block.acFrequencySF.orElse((short) 1), HERTZ));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_REAL_POWER),
getScaled(block.acRealPowerTotal, block.acRealPowerSF, WATT));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_APPARENT_POWER),
getScaled(block.acApparentPowerTotal, block.acApparentPowerSF, WATT)); // TODO: this should be VA
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_REACTIVE_POWER),
getScaled(block.acReactivePowerTotal, block.acReactivePowerSF, WATT)); // TODO: this should be VAR
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_AVERAGE_POWER_FACTOR),
getScaled(block.acPowerFactor, block.acPowerFactorSF, PERCENT));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_EXPORTED_REAL_ENERGY),
getScaled(block.acExportedRealEnergyTotal, block.acRealEnergySF, WATT_HOUR));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_IMPORTED_REAL_ENERGY),
getScaled(block.acImportedRealEnergyTotal, block.acRealEnergySF, WATT_HOUR));
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_EXPORTED_APPARENT_ENERGY),
getScaled(block.acExportedApparentEnergyTotal, block.acApparentEnergySF, WATT_HOUR)); // TODO: this
// should be
// VA_HOUR
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_IMPORTED_APPARENT_ENERGY),
getScaled(block.acImportedApparentEnergyTotal, block.acApparentEnergySF, WATT_HOUR)); // TODO: this
// should be
// VA_HOUR
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_IMPORTED_REACTIVE_ENERGY_Q1),
getScaled(block.acImportedReactiveEnergyQ1Total, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_IMPORTED_REACTIVE_ENERGY_Q2),
getScaled(block.acImportedReactiveEnergyQ2Total, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_EXPORTED_REACTIVE_ENERGY_Q3),
getScaled(block.acExportedReactiveEnergyQ3Total, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
updateState(channelUID(GROUP_AC_GENERAL, CHANNEL_AC_TOTAL_EXPORTED_REACTIVE_ENERGY_Q4),
getScaled(block.acExportedReactiveEnergyQ4Total, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
}
/**
* Update phase related channels for the selected phase.
*
* @param block the main block for scale
* @param phaseBlock the block containing the raw values for the selected phase
* @param group channel group id for the output
*/
private void updatePhaseValues(MeterModelBlock block, MeterModelBlock.PhaseBlock phaseBlock, String group) {
updateState(channelUID(group, CHANNEL_AC_PHASE_CURRENT),
getScaled(phaseBlock.acPhaseCurrent, block.acCurrentSF, AMPERE));
updateState(channelUID(group, CHANNEL_AC_VOLTAGE_TO_N),
getScaled(phaseBlock.acVoltageToN, block.acVoltageSF, VOLT));
updateState(channelUID(group, CHANNEL_AC_VOLTAGE_TO_NEXT),
getScaled(phaseBlock.acVoltageToNext, block.acVoltageSF, VOLT));
updateState(channelUID(group, CHANNEL_AC_REAL_POWER),
getScaled(phaseBlock.acRealPower, block.acRealPowerSF, WATT));
updateState(channelUID(group, CHANNEL_AC_APPARENT_POWER),
getScaled(phaseBlock.acApparentPower, block.acApparentPowerSF, WATT)); // TODO: this should be VA
updateState(channelUID(group, CHANNEL_AC_REACTIVE_POWER),
getScaled(phaseBlock.acReactivePower, block.acReactivePowerSF, WATT)); // TODO: this should be VAR
updateState(channelUID(group, CHANNEL_AC_POWER_FACTOR),
getScaled(phaseBlock.acPowerFactor, block.acPowerFactorSF, PERCENT));
updateState(channelUID(group, CHANNEL_AC_EXPORTED_REAL_ENERGY),
getScaled(phaseBlock.acExportedRealEnergy, block.acRealEnergySF, WATT_HOUR));
updateState(channelUID(group, CHANNEL_AC_IMPORTED_REAL_ENERGY),
getScaled(phaseBlock.acImportedRealEnergy, block.acRealEnergySF, WATT_HOUR));
updateState(channelUID(group, CHANNEL_AC_EXPORTED_APPARENT_ENERGY),
getScaled(phaseBlock.acExportedApparentEnergy, block.acApparentEnergySF, WATT_HOUR)); // TODO: this
// should be
// VA_HOUR
updateState(channelUID(group, CHANNEL_AC_IMPORTED_APPARENT_ENERGY),
getScaled(phaseBlock.acImportedApparentEnergy, block.acApparentEnergySF, WATT_HOUR)); // TODO: this
// should be
// VA_HOUR
updateState(channelUID(group, CHANNEL_AC_IMPORTED_REACTIVE_ENERGY_Q1),
getScaled(phaseBlock.acImportedReactiveEnergyQ1, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
updateState(channelUID(group, CHANNEL_AC_IMPORTED_REACTIVE_ENERGY_Q2),
getScaled(phaseBlock.acImportedReactiveEnergyQ2, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
updateState(channelUID(group, CHANNEL_AC_EXPORTED_REACTIVE_ENERGY_Q3),
getScaled(phaseBlock.acExportedReactiveEnergyQ3, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
updateState(channelUID(group, CHANNEL_AC_EXPORTED_REACTIVE_ENERGY_Q4),
getScaled(phaseBlock.acExportedReactiveEnergyQ4, block.acReactiveEnergySF, WATT_HOUR)); // TODO: this
// should be
// VAR_HOUR
}
}

View File

@@ -0,0 +1,126 @@
/**
* 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.modbus.sunspec.internal.parser;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.io.transport.modbus.ModbusBitUtilities;
import org.openhab.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
/**
* Base class for parsers with some helper methods
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
@NonNullByDefault
public class AbstractBaseParser {
/**
* Extract an optional int16 value
*
* @param raw the register array to extract from
* @param index the address of the field
* @return the parsed value or empty if the field is not implemented
*/
protected Optional<Short> extractOptionalInt16(ModbusRegisterArray raw, int index) {
return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.INT16).map(DecimalType::shortValue)
.filter(value -> value != (short) 0x8000);
}
/**
* Extract a mandatory int16 value
*
* @param raw the register array to extract from
* @param index the address of the field
* @param def the default value
* @return the parsed value or the default if the field is not implemented
*/
protected Short extractInt16(ModbusRegisterArray raw, int index, short def) {
return extractOptionalInt16(raw, index).orElse(def);
}
/**
* Extract an optional uint16 value
*
* @param raw the register array to extract from
* @param index the address of the field
* @return the parsed value or empty if the field is not implemented
*/
protected Optional<Integer> extractOptionalUInt16(ModbusRegisterArray raw, int index) {
return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.UINT16).map(DecimalType::intValue)
.filter(value -> value != 0xffff);
}
/**
* Extract a mandatory uint16 value
*
* @param raw the register array to extract from
* @param index the address of the field
* @param def the default value
* @return the parsed value or the default if the field is not implemented
*/
protected Integer extractUInt16(ModbusRegisterArray raw, int index, int def) {
return extractOptionalUInt16(raw, index).orElse(def);
}
/**
* Extract an optional acc32 value
*
* @param raw the register array to extract from
* @param index the address of the field
* @return the parsed value or empty if the field is not implemented
*/
protected Optional<Long> extractOptionalAcc32(ModbusRegisterArray raw, int index) {
return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.INT32).map(DecimalType::longValue)
.filter(value -> value != 0);
}
/**
* Extract a mandatory acc32 value
*
* @param raw the register array to extract from
* @param index the address of the field
* @param def the default value
* @return the parsed value or default if the field is not implemented
*/
protected Long extractAcc32(ModbusRegisterArray raw, int index, long def) {
return extractOptionalAcc32(raw, index).orElse(def);
}
/**
* Extract an optional scale factor
*
* @param raw the register array to extract from
* @param index the address of the field
* @return the parsed value or empty if the field is not implemented
*/
protected Optional<Short> extractOptionalSunSSF(ModbusRegisterArray raw, int index) {
return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.INT16).map(DecimalType::shortValue)
.filter(value -> value != (short) 0x8000);
}
/**
* Extract an mandatory scale factor
*
* @param raw the register array to extract from
* @param index the address of the field
* @return the parsed value or 1 if the field is not implemented
*/
protected Short extractSunSSF(ModbusRegisterArray raw, int index) {
return extractOptionalSunSSF(raw, index).orElse((short) 0);
}
}

View File

@@ -0,0 +1,66 @@
/**
* 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.modbus.sunspec.internal.parser;
import java.nio.charset.Charset;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.sunspec.internal.SunSpecConstants;
import org.openhab.binding.modbus.sunspec.internal.dto.CommonModelBlock;
import org.openhab.io.transport.modbus.ModbusBitUtilities;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parser for the Common Message block
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
@NonNullByDefault
public class CommonModelParser extends AbstractBaseParser implements SunspecParser<CommonModelBlock> {
/**
* Logger instance
*/
private final Logger logger = LoggerFactory.getLogger(CommonModelParser.class);
@Override
public CommonModelBlock parse(ModbusRegisterArray raw) {
CommonModelBlock block = new CommonModelBlock();
block.sunSpecDID = extractUInt16(raw, 0, 0);
block.length = extractUInt16(raw, 1, raw.size() - SunSpecConstants.MODEL_HEADER_SIZE);
if (block.length + SunSpecConstants.MODEL_HEADER_SIZE != raw.size()) {
logger.warn("Short read on common block loding. Expected size: {}, got: {}",
block.length + SunSpecConstants.MODEL_HEADER_SIZE, raw.size());
return block;
}
// parse manufacturer, model and version
block.manufacturer = ModbusBitUtilities.extractStringFromRegisters(raw, 2, 32, Charset.forName("UTF-8"))
.toString();
block.model = ModbusBitUtilities.extractStringFromRegisters(raw, 18, 32, Charset.forName("UTF-8")).toString();
block.version = ModbusBitUtilities.extractStringFromRegisters(raw, 42, 16, Charset.forName("UTF-8")).toString();
block.serialNumber = ModbusBitUtilities.extractStringFromRegisters(raw, 50, 32, Charset.forName("UTF-8"))
.toString();
block.deviceAddress = extractUInt16(raw, 66, 1);
return block;
}
}

View File

@@ -0,0 +1,79 @@
/**
* 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.modbus.sunspec.internal.parser;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.sunspec.internal.SunSpecConstants;
import org.openhab.binding.modbus.sunspec.internal.dto.InverterModelBlock;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
/**
* Parses inverter modbus data into an InverterModelBlock
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
@NonNullByDefault
public class InverterModelParser extends AbstractBaseParser implements SunspecParser<InverterModelBlock> {
@Override
public InverterModelBlock parse(ModbusRegisterArray raw) {
InverterModelBlock block = new InverterModelBlock();
block.phaseConfiguration = extractUInt16(raw, 0, SunSpecConstants.INVERTER_SINGLE_PHASE);
block.length = extractUInt16(raw, 1, raw.size());
block.acCurrentTotal = extractUInt16(raw, 2, 0);
block.acCurrentPhaseA = extractUInt16(raw, 3, 0);
block.acCurrentPhaseB = extractOptionalUInt16(raw, 4);
block.acCurrentPhaseC = extractOptionalUInt16(raw, 5);
block.acCurrentSF = extractSunSSF(raw, 6);
block.acVoltageAB = extractOptionalUInt16(raw, 7);
block.acVoltageBC = extractOptionalUInt16(raw, 8);
block.acVoltageCA = extractOptionalUInt16(raw, 9);
block.acVoltageAtoN = extractUInt16(raw, 10, 0);
block.acVoltageBtoN = extractOptionalUInt16(raw, 11);
block.acVoltageCtoN = extractOptionalUInt16(raw, 12);
block.acVoltageSF = extractSunSSF(raw, 13);
block.acPower = extractInt16(raw, 14, (short) 0);
block.acPowerSF = extractSunSSF(raw, 15);
block.acFrequency = extractUInt16(raw, 16, 0);
block.acFrequencySF = extractSunSSF(raw, 17);
block.acApparentPower = extractOptionalInt16(raw, 18);
block.acApparentPowerSF = extractOptionalSunSSF(raw, 19);
block.acReactivePower = extractOptionalInt16(raw, 20);
block.acReactivePowerSF = extractOptionalSunSSF(raw, 21);
block.acPowerFactor = extractOptionalInt16(raw, 22);
block.acPowerFactorSF = extractOptionalSunSSF(raw, 23);
block.acEnergyLifetime = extractAcc32(raw, 24, 0);
block.acEnergyLifetimeSF = extractSunSSF(raw, 26);
block.dcCurrent = extractOptionalUInt16(raw, 27);
block.dcCurrentSF = extractOptionalSunSSF(raw, 28);
block.dcVoltage = extractOptionalUInt16(raw, 29);
block.dcVoltageSF = extractOptionalSunSSF(raw, 30);
block.dcPower = extractOptionalInt16(raw, 31);
block.dcPowerSF = extractOptionalSunSSF(raw, 32);
block.temperatureCabinet = extractInt16(raw, 33, (short) 0);
block.temperatureHeatsink = extractOptionalInt16(raw, 34);
block.temperatureTransformer = extractOptionalInt16(raw, 35);
block.temperatureOther = extractOptionalInt16(raw, 36);
block.temperatureSF = extractSunSSF(raw, 37);
block.status = extractUInt16(raw, 38, 1);
block.statusVendor = extractOptionalUInt16(raw, 39);
return block;
}
}

View File

@@ -0,0 +1,118 @@
/**
* 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.modbus.sunspec.internal.parser;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.sunspec.internal.SunSpecConstants;
import org.openhab.binding.modbus.sunspec.internal.dto.MeterModelBlock;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
/**
* Parser for sunspec compatible meters
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
@NonNullByDefault
public class MeterModelParser extends AbstractBaseParser implements SunspecParser<MeterModelBlock> {
@Override
public MeterModelBlock parse(ModbusRegisterArray raw) {
MeterModelBlock block = new MeterModelBlock();
block.sunspecDID = extractUInt16(raw, 0, SunSpecConstants.METER_SINGLE_PHASE);
block.length = extractUInt16(raw, 1, raw.size());
block.acCurrentTotal = extractInt16(raw, 2, (short) 0);
block.phaseA.acPhaseCurrent = extractOptionalInt16(raw, 3);
block.phaseB.acPhaseCurrent = extractOptionalInt16(raw, 4);
block.phaseC.acPhaseCurrent = extractOptionalInt16(raw, 5);
block.acCurrentSF = extractSunSSF(raw, 6);
block.acVoltageLineToNAverage = extractOptionalInt16(raw, 7);
block.phaseA.acVoltageToN = extractOptionalInt16(raw, 8);
block.phaseB.acVoltageToN = extractOptionalInt16(raw, 9);
block.phaseC.acVoltageToN = extractOptionalInt16(raw, 10);
block.acVoltageLineToLineAverage = extractOptionalInt16(raw, 11);
block.phaseA.acVoltageToNext = extractOptionalInt16(raw, 12);
block.phaseB.acVoltageToNext = extractOptionalInt16(raw, 13);
block.phaseC.acVoltageToNext = extractOptionalInt16(raw, 14);
block.acVoltageSF = extractSunSSF(raw, 15);
block.acFrequency = extractInt16(raw, 16, (short) 0);
block.acFrequencySF = extractOptionalSunSSF(raw, 17);
block.acRealPowerTotal = extractInt16(raw, 18, (short) 0);
block.phaseA.acRealPower = extractOptionalInt16(raw, 19);
block.phaseB.acRealPower = extractOptionalInt16(raw, 20);
block.phaseC.acRealPower = extractOptionalInt16(raw, 21);
block.acRealPowerSF = extractSunSSF(raw, 22);
block.acApparentPowerTotal = extractOptionalInt16(raw, 23);
block.phaseA.acApparentPower = extractOptionalInt16(raw, 24);
block.phaseB.acApparentPower = extractOptionalInt16(raw, 25);
block.phaseC.acApparentPower = extractOptionalInt16(raw, 26);
block.acApparentPowerSF = extractOptionalSunSSF(raw, 27);
block.acReactivePowerTotal = extractOptionalInt16(raw, 28);
block.phaseA.acReactivePower = extractOptionalInt16(raw, 29);
block.phaseB.acReactivePower = extractOptionalInt16(raw, 30);
block.phaseC.acReactivePower = extractOptionalInt16(raw, 31);
block.acReactivePowerSF = extractOptionalSunSSF(raw, 32);
block.acPowerFactor = extractOptionalInt16(raw, 33);
block.phaseA.acPowerFactor = extractOptionalInt16(raw, 34);
block.phaseB.acPowerFactor = extractOptionalInt16(raw, 35);
block.phaseC.acPowerFactor = extractOptionalInt16(raw, 36);
block.acPowerFactorSF = extractOptionalSunSSF(raw, 37);
block.acExportedRealEnergyTotal = extractOptionalAcc32(raw, 38);
block.phaseA.acExportedRealEnergy = extractOptionalAcc32(raw, 40);
block.phaseB.acExportedRealEnergy = extractOptionalAcc32(raw, 42);
block.phaseC.acExportedRealEnergy = extractOptionalAcc32(raw, 44);
block.acImportedRealEnergyTotal = extractAcc32(raw, 46, 0);
block.phaseA.acImportedRealEnergy = extractOptionalAcc32(raw, 48);
block.phaseB.acImportedRealEnergy = extractOptionalAcc32(raw, 50);
block.phaseC.acImportedRealEnergy = extractOptionalAcc32(raw, 52);
block.acRealEnergySF = extractSunSSF(raw, 54);
block.acExportedApparentEnergyTotal = extractOptionalAcc32(raw, 55);
block.phaseA.acExportedApparentEnergy = extractOptionalAcc32(raw, 57);
block.phaseB.acExportedApparentEnergy = extractOptionalAcc32(raw, 59);
block.phaseC.acExportedApparentEnergy = extractOptionalAcc32(raw, 61);
block.acImportedApparentEnergyTotal = extractOptionalAcc32(raw, 63);
block.phaseA.acImportedApparentEnergy = extractOptionalAcc32(raw, 65);
block.phaseB.acImportedApparentEnergy = extractOptionalAcc32(raw, 67);
block.phaseC.acImportedApparentEnergy = extractOptionalAcc32(raw, 69);
block.acApparentEnergySF = extractOptionalSunSSF(raw, 71);
block.acImportedReactiveEnergyQ1Total = extractOptionalAcc32(raw, 72);
block.phaseA.acImportedReactiveEnergyQ1 = extractOptionalAcc32(raw, 74);
block.phaseB.acImportedReactiveEnergyQ1 = extractOptionalAcc32(raw, 76);
block.phaseC.acImportedReactiveEnergyQ1 = extractOptionalAcc32(raw, 78);
block.acImportedReactiveEnergyQ2Total = extractOptionalAcc32(raw, 80);
block.phaseA.acImportedReactiveEnergyQ2 = extractOptionalAcc32(raw, 82);
block.phaseB.acImportedReactiveEnergyQ2 = extractOptionalAcc32(raw, 84);
block.phaseC.acImportedReactiveEnergyQ2 = extractOptionalAcc32(raw, 86);
block.acExportedReactiveEnergyQ3Total = extractOptionalAcc32(raw, 88);
block.phaseA.acExportedReactiveEnergyQ3 = extractOptionalAcc32(raw, 90);
block.phaseB.acExportedReactiveEnergyQ3 = extractOptionalAcc32(raw, 92);
block.phaseC.acExportedReactiveEnergyQ3 = extractOptionalAcc32(raw, 94);
block.acExportedReactiveEnergyQ4Total = extractOptionalAcc32(raw, 96);
block.phaseA.acExportedReactiveEnergyQ4 = extractOptionalAcc32(raw, 98);
block.phaseB.acExportedReactiveEnergyQ4 = extractOptionalAcc32(raw, 100);
block.phaseC.acExportedReactiveEnergyQ4 = extractOptionalAcc32(raw, 102);
block.acReactiveEnergySF = extractOptionalSunSSF(raw, 104);
return block;
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.modbus.sunspec.internal.parser;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.io.transport.modbus.ModbusRegisterArray;
/**
* General interface for sunspec parsers
*
* Parsers are responsible to take the raw register array
* that was read from the device and to parse them into a SunSpecMessageBlock
* They should parse all reasonable fields into separate properties
* in the message block.
*
* Fields with unsupported values should be parsed as null values.
*
* In no way should a parser handle value scaling or device specific
* workarounds. These should be done in the handler.
*
* @author Nagy Attila Gabor - Initial contribution
*
*/
@NonNullByDefault
public interface SunspecParser<T> {
/**
* This method should parser an incoming register array and
* return a not-null sunspec block
*/
T parse(ModbusRegisterArray raw);
}

View File

@@ -0,0 +1,38 @@
<?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:sunspec:modbusconfig">
<parameter name="refresh" type="integer" min="0" unit="s">
<label>Refresh Interval</label>
<description>Poll interval. Use zero to disable automatic polling.</description>
<default>5</default>
</parameter>
<parameter name="address" type="integer" min="0" max="65535">
<label>Start Address</label>
<description>Start address of the model block</description>
<default>40000</default>
<advanced>true</advanced>
</parameter>
<parameter name="length" type="integer" min="1" max="65535">
<label>Block Length</label>
<description>Length of the model block in 2 byte words</description>
<default>61</default>
<advanced>true</advanced>
</parameter>
<parameter name="maxTries" type="integer" min="1">
<label>Maximum Tries When Reading</label>
<default>3</default>
<description>Number of tries when reading data, if some of the reading fail. For single try, enter 1.</description>
<advanced>true</advanced>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="modbus"
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-group-type id="device-information">
<label>Device Information</label>
<channels>
<channel id="cabinet-temperature" typeId="cabinet-temperature-type"/>
<channel id="heatsink-temperature" typeId="heatsink-temperature-type"/>
<channel id="transformer-temperature" typeId="transformer-temperature-type"/>
<channel id="other-temperature" typeId="other-temperature-type"/>
<channel id="status" typeId="status-type"/>
</channels>
</channel-group-type>
<channel-group-type id="ac-general">
<label>AC Summary</label>
<channels>
<channel id="ac-total-current" typeId="ac-total-current-type"/>
<channel id="ac-power" typeId="ac-power-type"/>
<channel id="ac-frequency" typeId="ac-frequency-type"/>
<channel id="ac-apparent-power" typeId="ac-apparent-power-type"/>
<channel id="ac-reactive-power" typeId="ac-reactive-power-type"/>
<channel id="ac-power-factor" typeId="ac-power-factor-type"/>
<channel id="ac-lifetime-energy" typeId="ac-lifetime-energy-type"/>
</channels>
</channel-group-type>
<channel-group-type id="ac-phase">
<label>AC Phase</label>
<channels>
<channel id="ac-phase-current" typeId="ac-phase-current-type"/>
<channel id="ac-voltage-to-next" typeId="ac-voltage-to-next-type"/>
<channel id="ac-voltage-to-n" typeId="ac-voltage-to-n-type"/>
</channels>
</channel-group-type>
<channel-group-type id="dc-general">
<label>DC summary</label>
<channels>
<channel id="dc-current" typeId="dc-current-type"/>
<channel id="dc-voltage" typeId="dc-voltage-type"/>
<channel id="dc-power" typeId="dc-power-type"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="modbus"
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="ac-total-current-type">
<item-type>Number:ElectricCurrent</item-type>
<label>AC Total Current Value</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="ac-phase-current-type">
<item-type>Number:ElectricCurrent</item-type>
<label>AC Phase Current Value</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="ac-voltage-to-next-type">
<item-type>Number:ElectricPotential</item-type>
<label>AC Voltage</label>
<description>This phase's AC voltage relative to the next phase</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-voltage-to-n-type">
<item-type>Number:ElectricPotential</item-type>
<label>AC Voltage Phase To N Value</label>
<description>This phase's AC voltage relative to N line</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-power-type">
<item-type>Number:Power</item-type>
<label>AC Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-frequency-type">
<item-type>Number:Frequency</item-type>
<label>AC Frequency Value</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="ac-apparent-power-type" advanced="true">
<item-type>Number:Power</item-type>
<label>AC Apparent Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-reactive-power-type" advanced="true">
<item-type>Number:Power</item-type>
<label>AC Reactive Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-power-factor-type">
<item-type>Number:Dimensionless</item-type>
<label>AC Power Factor</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="ac-lifetime-energy-type">
<item-type>Number:Energy</item-type>
<label>AC Lifetime Energy Production</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="dc-current-type">
<item-type>Number:ElectricCurrent</item-type>
<label>DC Current Value</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="dc-voltage-type">
<item-type>Number:ElectricPotential</item-type>
<label>DC Voltage Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="dc-power-type">
<item-type>Number:Power</item-type>
<label>DC Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="cabinet-temperature-type">
<item-type>Number:Temperature</item-type>
<label>Cabinet Temperature</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="heatsink-temperature-type">
<item-type>Number:Temperature</item-type>
<label>Heat Sink Temperature</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="transformer-temperature-type">
<item-type>Number:Temperature</item-type>
<label>Transformer Temperature</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="other-temperature-type">
<item-type>Number:Temperature</item-type>
<label>Other Temperature</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="status-type">
<item-type>String</item-type>
<label>Status</label>
<description>Device status</description>
<state readOnly="true">
<options>
<option value="OFF">Off</option>
<option value="SLEEP">Sleeping / Night mode</option>
<option value="ON">On - producing power</option>
</options>
</state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="modbus"
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="inverter-single-phase">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
<bridge-type-ref id="serial"/>
</supported-bridge-type-refs>
<label>Single Phase Inverter</label>
<description>Single phase inverter supporting SunSpec mapping over tcp modbus connection.</description>
<category>Inverter</category>
<channel-groups>
<channel-group id="deviceInformation" typeId="device-information"/>
<channel-group id="acGeneral" typeId="ac-general"/>
<channel-group id="acPhaseA" typeId="ac-phase">
<label>AC Detail</label>
</channel-group>
<channel-group id="dcGeneral" typeId="dc-general"/>
</channel-groups>
<properties>
<property name="vendor"/>
<property name="model"/>
<property name="version"/>
<property name="serialNumber"/>
<property name="uniqueAddress"/>
</properties>
<representation-property>uniqueAddress</representation-property>
<config-description-ref uri="thing-type:sunspec:modbusconfig"/>
</thing-type>
<thing-type id="inverter-split-phase">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
<bridge-type-ref id="serial"/>
</supported-bridge-type-refs>
<label>Split Phase Inverter</label>
<description>Split phase (Japanese grid and 240V grid in North America) inverter supporting SunSpec mapping over tcp
modbus connection</description>
<category>Inverter</category>
<channel-groups>
<channel-group id="deviceInformation" typeId="device-information"/>
<channel-group id="acGeneral" typeId="ac-general"/>
<channel-group id="acPhaseA" typeId="ac-phase">
<label>AC Phase A (L1)</label>
</channel-group>
<channel-group id="acPhaseB" typeId="ac-phase">
<label>AC Phase B (L2)</label>
</channel-group>
<channel-group id="dcGeneral" typeId="dc-general"/>
</channel-groups>
<properties>
<property name="vendor"/>
<property name="model"/>
<property name="version"/>
<property name="serialNumber"/>
<property name="uniqueAddress"/>
</properties>
<representation-property>uniqueAddress</representation-property>
<config-description-ref uri="thing-type:sunspec:modbusconfig"/>
</thing-type>
<thing-type id="inverter-three-phase">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
<bridge-type-ref id="serial"/>
</supported-bridge-type-refs>
<label>Three Phase Inverter</label>
<description>Three phase inverter supporting SunSpec mapping over tcp modbus connection</description>
<category>Inverter</category>
<channel-groups>
<channel-group id="deviceInformation" typeId="device-information"/>
<channel-group id="acGeneral" typeId="ac-general"/>
<channel-group id="acPhaseA" typeId="ac-phase">
<label>AC Phase A (L1)</label>
</channel-group>
<channel-group id="acPhaseB" typeId="ac-phase">
<label>AC Phase B (L2)</label>
</channel-group>
<channel-group id="acPhaseC" typeId="ac-phase">
<label>AC Phase C (L3)</label>
</channel-group>
<channel-group id="dcGeneral" typeId="dc-general"/>
</channel-groups>
<properties>
<property name="vendor"/>
<property name="model"/>
<property name="version"/>
<property name="serialNumber"/>
<property name="uniqueAddress"/>
</properties>
<representation-property>uniqueAddress</representation-property>
<config-description-ref uri="thing-type:sunspec:modbusconfig"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="modbus"
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-group-type id="meter-ac-general">
<label>Summary</label>
<channels>
<channel id="ac-total-current" typeId="ac-total-current-type"/>
<channel id="ac-average-voltage-to-n" typeId="ac-average-voltage-to-n-type"/>
<channel id="ac-average-voltage-to-next" typeId="ac-average-voltage-to-next-type"/>
<channel id="ac-frequency" typeId="ac-frequency-type"/>
<channel id="ac-total-real-power" typeId="ac-total-real-power-type"/>
<channel id="ac-total-apparent-power" typeId="ac-total-apparent-power-type"/>
<channel id="ac-total-reactive-power" typeId="ac-total-reactive-power-type"/>
<channel id="ac-average-power-factor" typeId="ac-average-power-factor-type"/>
<channel id="ac-total-exported-real-energy" typeId="ac-total-exported-real-energy-type"/>
<channel id="ac-total-imported-real-energy" typeId="ac-total-imported-real-energy-type"/>
<channel id="ac-total-exported-apparent-energy" typeId="ac-total-exported-apparent-energy-type"/>
<channel id="ac-total-imported-apparent-energy" typeId="ac-total-imported-apparent-energy-type"/>
<channel id="ac-total-imported-reactive-energy-q1" typeId="ac-total-imported-reactive-energy-q1-type"/>
<channel id="ac-total-imported-reactive-energy-q2" typeId="ac-total-imported-reactive-energy-q2-type"/>
<channel id="ac-total-exported-reactive-energy-q3" typeId="ac-total-exported-reactive-energy-q3-type"/>
<channel id="ac-total-exported-reactive-energy-q4" typeId="ac-total-exported-reactive-energy-q4-type"/>
</channels>
</channel-group-type>
<channel-group-type id="meter-ac-phase">
<label>Phase</label>
<channels>
<channel id="ac-phase-current" typeId="ac-phase-current-type"/>
<channel id="ac-voltage-to-n" typeId="ac-voltage-to-n-type"/>
<channel id="ac-voltage-to-next" typeId="ac-voltage-to-next-type"/>
<channel id="ac-real-power" typeId="ac-real-power-type"/>
<channel id="ac-apparent-power" typeId="ac-apparent-power-type"/>
<channel id="ac-reactive-power" typeId="ac-reactive-power-type"/>
<channel id="ac-power-factor" typeId="ac-power-factor-type"/>
<channel id="ac-exported-real-energy" typeId="ac-exported-real-energy-type"/>
<channel id="ac-imported-real-energy" typeId="ac-imported-real-energy-type"/>
<channel id="ac-exported-apparent-energy" typeId="ac-exported-apparent-energy-type"/>
<channel id="ac-imported-apparent-energy" typeId="ac-imported-apparent-energy-type"/>
<channel id="ac-imported-reactive-energy-q1" typeId="ac-imported-reactive-energy-q1-type"/>
<channel id="ac-imported-reactive-energy-q2" typeId="ac-imported-reactive-energy-q2-type"/>
<channel id="ac-exported-reactive-energy-q3" typeId="ac-exported-reactive-energy-q3-type"/>
<channel id="ac-exported-reactive-energy-q4" typeId="ac-exported-reactive-energy-q4-type"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="modbus"
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="ac-average-voltage-to-n-type">
<item-type>Number:ElectricPotential</item-type>
<label>Average Line To Neutral AC Voltage</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-average-voltage-to-next-type">
<item-type>Number:ElectricPotential</item-type>
<label>Average Line To Line AC Voltage</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-real-power-type">
<item-type>Number:Power</item-type>
<label>Total Real Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-real-power-type">
<item-type>Number:Power</item-type>
<label>AC Real Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-apparent-power-type">
<item-type>Number:Power</item-type>
<label>Total Apparent Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-reactive-power-type">
<item-type>Number:Power</item-type>
<label>Total Reactive Power Value</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-average-power-factor-type">
<item-type>Number:Dimensionless</item-type>
<label>Average AC Power Factor</label>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="ac-total-exported-real-energy-type">
<item-type>Number:Energy</item-type>
<label>Total Real Energy Exported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-exported-real-energy-type">
<item-type>Number:Energy</item-type>
<label>Real Energy Exported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-imported-real-energy-type">
<item-type>Number:Energy</item-type>
<label>Total Real Energy Imported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-imported-real-energy-type">
<item-type>Number:Energy</item-type>
<label>Real Energy Imported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-exported-apparent-energy-type">
<item-type>Number:Energy</item-type>
<label>Total Apparent Energy Exported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-exported-apparent-energy-type">
<item-type>Number:Energy</item-type>
<label>Apparent Energy Exported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-imported-apparent-energy-type">
<item-type>Number:Energy</item-type>
<label>Total Apparent Energy Imported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-imported-apparent-energy-type">
<item-type>Number:Energy</item-type>
<label>Apparent Energy Imported</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-imported-reactive-energy-q1-type">
<item-type>Number:Energy</item-type>
<label>Total Reactive Energy Imported Quadrant 1</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-imported-reactive-energy-q1-type">
<item-type>Number:Energy</item-type>
<label>Reactive Energy Imported Quadrant 1</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-imported-reactive-energy-q2-type">
<item-type>Number:Energy</item-type>
<label>Total Reactive Energy Imported Quadrant 2</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-imported-reactive-energy-q2-type">
<item-type>Number:Energy</item-type>
<label>Reactive Energy Imported Quadrant 2</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-exported-reactive-energy-q3-type">
<item-type>Number:Energy</item-type>
<label>Total Reactive Energy Exported Quadrant 3</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-exported-reactive-energy-q3-type">
<item-type>Number:Energy</item-type>
<label>Reactive Energy Exported Quadrant 3</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-total-exported-reactive-energy-q4-type">
<item-type>Number:Energy</item-type>
<label>Total Reactive Energy Exported Quadrant 4</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ac-exported-reactive-energy-q4-type">
<item-type>Number:Energy</item-type>
<label>Reactive Energy Exported Quadrant 4</label>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="modbus"
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="meter-single-phase">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
<bridge-type-ref id="serial"/>
</supported-bridge-type-refs>
<label>Single Phase Meter</label>
<description>Single phase (AN or AB) meter supporting SunSpec mapping over modbus connection</description>
<category>Meter</category>
<channel-groups>
<channel-group id="acGeneral" typeId="meter-ac-general"/>
<channel-group id="acPhaseA" typeId="meter-ac-phase">
<label>AC Phase A</label>
</channel-group>
</channel-groups>
<properties>
<property name="vendor"/>
<property name="model"/>
<property name="version"/>
<property name="serialNumber"/>
<property name="uniqueAddress"/>
</properties>
<representation-property>uniqueAddress</representation-property>
<config-description-ref uri="thing-type:sunspec:modbusconfig"/>
</thing-type>
<thing-type id="meter-split-phase">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
<bridge-type-ref id="serial"/>
</supported-bridge-type-refs>
<label>Split Phase Meter</label>
<description>Split phase (ABN) meter supporting SunSpec mapping over modbus connection</description>
<category>Meter</category>
<channel-groups>
<channel-group id="acGeneral" typeId="meter-ac-general"/>
<channel-group id="acPhaseA" typeId="meter-ac-phase">
<label>AC Phase A</label>
</channel-group>
<channel-group id="acPhaseB" typeId="meter-ac-phase">
<label>AC Phase B</label>
</channel-group>
</channel-groups>
<properties>
<property name="vendor"/>
<property name="model"/>
<property name="version"/>
<property name="serialNumber"/>
<property name="uniqueAddress"/>
</properties>
<representation-property>uniqueAddress</representation-property>
<config-description-ref uri="thing-type:sunspec:modbusconfig"/>
</thing-type>
<thing-type id="meter-wye-phase">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
<bridge-type-ref id="serial"/>
</supported-bridge-type-refs>
<label>Three Phase Meter, Wye-Connected</label>
<description>Wye-connected three phase (ABCN) meter supporting SunSpec mapping over modbus connection</description>
<category>Meter</category>
<channel-groups>
<channel-group id="acGeneral" typeId="meter-ac-general"/>
<channel-group id="acPhaseA" typeId="meter-ac-phase">
<label>AC Phase A</label>
</channel-group>
<channel-group id="acPhaseB" typeId="meter-ac-phase">
<label>AC Phase B</label>
</channel-group>
<channel-group id="acPhaseC" typeId="meter-ac-phase">
<label>AC Phase C</label>
</channel-group>
</channel-groups>
<properties>
<property name="vendor"/>
<property name="model"/>
<property name="version"/>
<property name="serialNumber"/>
<property name="uniqueAddress"/>
</properties>
<representation-property>uniqueAddress</representation-property>
<config-description-ref uri="thing-type:sunspec:modbusconfig"/>
</thing-type>
<thing-type id="meter-delta-phase">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
<bridge-type-ref id="serial"/>
</supported-bridge-type-refs>
<label>Three Phase Meter, Delta-Connected</label>
<description>Delta-connected three phase (ABC) meter supporting SunSpec mapping over modbus connection</description>
<category>Meter</category>
<channel-groups>
<channel-group id="acGeneral" typeId="meter-ac-general"/>
<channel-group id="acPhaseA" typeId="meter-ac-phase">
<label>AC Phase A</label>
</channel-group>
<channel-group id="acPhaseB" typeId="meter-ac-phase">
<label>AC Phase B</label>
</channel-group>
<channel-group id="acPhaseC" typeId="meter-ac-phase">
<label>AC Phase C</label>
</channel-group>
</channel-groups>
<properties>
<property name="vendor"/>
<property name="model"/>
<property name="version"/>
<property name="serialNumber"/>
<property name="uniqueAddress"/>
</properties>
<representation-property>uniqueAddress</representation-property>
<config-description-ref uri="thing-type:sunspec:modbusconfig"/>
</thing-type>
</thing:thing-descriptions>