[modbus.foxinverter] Initial contribution (#19637)

* Initial Contribution: modbus.foxinverter

Signed-off-by: Holger Friedrich <mail@holger-friedrich.de>
This commit is contained in:
Holger Friedrich
2025-11-22 16:11:16 +01:00
committed by GitHub
parent c9fe5077b1
commit 5c368e3d98
17 changed files with 1858 additions and 0 deletions
+1
View File
@@ -243,6 +243,7 @@
/bundles/org.openhab.binding.minecraft/ @ibaton
/bundles/org.openhab.binding.modbus/ @ssalonen
/bundles/org.openhab.binding.modbus.e3dc/ @weymann
/bundles/org.openhab.binding.modbus.foxinverter/ @holgerfriedrich
/bundles/org.openhab.binding.modbus.helioseasycontrols/ @bern77
/bundles/org.openhab.binding.modbus.kermi/ @KaaNee
/bundles/org.openhab.binding.modbus.sbc/ @fwolter
+5
View File
@@ -1201,6 +1201,11 @@
<artifactId>org.openhab.binding.modbus.e3dc</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.modbus.foxinverter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.modbus.helioseasycontrols</artifactId>
@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
@@ -0,0 +1,184 @@
# Modbus FoxInverter Binding
This binding integrates the FoxESS solar inverters into openHAB.
## Supported Inverters
As FoxESS inverters are sold in slightly different configurations, inverters of several brands might be supported.
This one is tested:
- Solakon ONE
Others are untested, but might work:
- FoxESS Avocado 22 Pro
## Supported Things
The binding supports only one thing:
- `mq2200-inverter`: The FoxESS MQ-2200 inverter (also sold with the product name Solakon ONE or Avocado 22 Pro), connected via Modbus TCP.
## Preparation
The data from the inverter is read via Modbus. So you need to configure a Modbus TCP Slave `tcp` as bridge first.
The inverter can be connected via LAN or WLAN.
As of now, you need to get the IP address of the inverter from your Router.
IP is obtained via DHCP.
I have not found a way to set a static IP on the Solakon ONE.
To troubleshoot, it is recommended to open a telnet connection to port 502. If it connects, you have likely found your device with an active Modbus server.
## Thing Configuration
Once you've configured the Modbus TCP Slave as Bridge, you can configure the mq2200-inverter thing.
You just have to select the configured bridge and optionally configure the polling interval.
The foxinverter binding supports autodiscovery, so if you enable discovery in the bridge options, you can just press the `scan` button to add your inverter to the inbox.
### FoxESS MQ-2200 Inverter (`mq2200-inverter`)
| Name | Type | Description | Default | Required | Advanced |
|---------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|----------|
| pollInterval | integer | Interval the device is polled in ms. | 5000 | yes | no |
| maxTries | integer | Specifies how many times the binding should retry reading data if a read attempt fails. <br/>Set to `1` to disable retries and use a single attempt. | 3 | yes | no |
## Channels
> NOTE: All channels are currently read-only. Writing is not yet implemented.
The `mq2200-inverter` thing has channels that serve the current state of the FoxESS inverter, as you are used to from the vendor app.
| Channel Type ID | Item Type | Description | Advanced | Channel Group |
|------------------------------------|--------------------------|---------------------------------------|-----------|---------------------|
| fi-inverter-power | Number:Power | Inverter Active Power | no | Overview |
| fi-home-import-power | Number:Power | Power flow with home network | no | Overview |
| fi-daily-pv-generation | Number:Energy | Daily PV Generation | no | Overview |
| fi-total-pv-generation | Number:Energy | Total PV Generation | no | Overview |
| fi-internal-temperature | Number:Temperature | Internal Temperature | yes | Overview |
| fi-meter-connected | Contact | Smart Meter is Connected | yes | Overview |
| fi-status-alarm | Contact | System Warning | no | Overview |
| fi-status-operation | Contact | System in Operational Mode | no | Overview |
| fi-status-standby | Contact | System in Standby Mode (not yet off) | yes | Overview |
| fi-pv-power | Number:Power | PV Power | no | MPPT Information |
| fi-mppt1-voltage | Number:ElectricPotential | MPPT1 Voltage | yes | MPPT Information |
| fi-mppt1-current | Number:ElectricCurrent | MPPT1 Current | yes | MPPT Information |
| fi-mppt1-power | Number:Power | MPPT1 Power | no | MPPT Information |
| fi-mppt2-voltage | Number:ElectricPotential | MPPT2 Voltage | yes | MPPT Information |
| fi-mppt2-current | Number:ElectricCurrent | MPPT2 Current | yes | MPPT Information |
| fi-mppt2-power | Number:Power | MPPT2 Power | no | MPPT Information |
| fi-mppt3-voltage | Number:ElectricPotential | MPPT3 Voltage | yes | MPPT Information |
| fi-mppt3-current | Number:ElectricCurrent | MPPT3 Current | yes | MPPT Information |
| fi-mppt3-power | Number:Power | MPPT3 Power | no | MPPT Information |
| fi-mppt4-voltage | Number:ElectricPotential | MPPT4 Voltage | yes | MPPT Information |
| fi-mppt4-current | Number:ElectricCurrent | MPPT4 Current | yes | MPPT Information |
| fi-mppt4-power | Number:Power | MPPT4 Power | no | MPPT Information |
| fi-battery-voltage | Number:ElectricPotential | Battery Voltage | yes | Battery Information |
| fi-battery-current | Number:ElectricCurrent | Battery Current | yes | Battery Information |
| fi-battery-level | Number:Dimensionless | Battery Level | no | Battery Information |
| fi-battery-charging-power | Number:Power | Battery Charging Power | no | Battery Information |
| fi-battery-temperature | Number:Temperature | Internal Temperature | no | Battery Information |
| fi-battery-minimum-soc | Number:Dimensionless | Battery Min Charging Level | yes | Battery Information |
| fi-battery-maximum-soc | Number:Dimensionless | Battery Max Charging Level | yes | Battery Information |
| fi-battery-minimum-soc-on-grid | Number:Dimensionless | Battery Min Charging Level on Grid | yes | Battery Information |
| fi-phase-a-voltage | Number:ElectricPotential | Phase A Voltage | yes | Grid Information |
| fi-phase-b-voltage | Number:ElectricPotential | Phase B Voltage | yes | Grid Information |
| fi-phase-c-voltage | Number:ElectricPotential | Phase C Voltage | yes | Grid Information |
| fi-grid-frequency | Number:Frequency | Grid Frequency | yes | Grid Information |
| fi-grid-export-power | Number:Power | Overall Power Export to Grid | no | Grid Information |
| fi-status-on-grid | Contact | Power Grid Available | yes | Grid Information |
| fi-eps-output | Switch | Enable EPS Output | no | EPS |
| fi-eps-export-power | Number:Power | EPS Output Power | no | EPS |
## Full Example
This example shows how to configure a FoxESS inverter connected via Modbus and uses the most common channels.
### mq2200.things
```java
Bridge modbus:tcp:powerplant [ host="10.0.0.2", port=502, id=1, enableDiscovery=false ] {
Thing mq2200-inverter inverter "FoxESS Inverter" [ pollInterval=5000 ]
}
```
### mq2200.items
```java
// Groups
Group inverter "FoxESS Inverter" <solarplant> ["Inverter"]
Group overview "Overview" <line> (inverter)
Group mppt "MPPT Information" <solarplant> (inverter)
Group batteryInformation "Battery information" <battery> (inverter)
Group gridInformation "Grid information" <energy> (inverter)
Group eps "Emergency Power Supply" <energy> (inverter)
// Overview
Number:Power active_power "Inverter Power" <line> (overview) ["Measurement", "Power"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-inverter-power"}
Number:Power battery_power "Home Import Power" <line> (batteryInformation,overview) ["Measurement", "Power"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-home-import-power"}
Number:Energy daily_pv_generation "Daily PV Generation" <line> (overview) ["Measurement", "Energy"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-daily-pv-generation"}
Number:Energy total_pv_generation "Total PV Generation" <line> (overview) ["Measurement", "Energy"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-total-pv-generation"}
Number:Temperature internal_temperature "Internal Temperature" <temperature> (overview) ["Measurement", "Temperature"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-internal-temperature"}
Contact meter_connected "Meter is Connected" (overview) ["Measurement"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-meter-connected"}
Contact status_alarm "Status Alarm" (overview) ["Measurement"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-status-alarm"}
Contact status_operation "Status Operation" (overview) ["Measurement"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-status-operation"}
Contact status_standby "Status Standby" (overview) ["Measurement"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-overview#fi-status-standby"}
// MPPT information
Number:Power pv_power "PV Power" <line> (mppt,overview) ["Measurement", "Power"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-pv-power"}
Number:ElectricPotential mppt1_voltage "MPPT1 Voltage" <energy> (mppt) ["Measurement", "Voltage"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt1-voltage"}
Number:ElectricCurrent mppt1_current "MPPT1 Current" <energy> (mppt) ["Measurement", "Current"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt1-current"}
Number:Power mppt1_power "MPPT1 Power" <line> (mppt) ["Measurement", "Power"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt1-power"}
Number:ElectricPotential mppt2_voltage "MPPT2 Voltage" <energy> (mppt) ["Measurement", "Voltage"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt2-voltage"}
Number:ElectricCurrent mppt2_current "MPPT2 Current" <energy> (mppt) ["Measurement", "Current"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt2-current"}
Number:Power mppt2_power "MPPT2 Power" <line> (mppt) ["Measurement", "Power"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt2-power"}
Number:ElectricPotential mppt3_voltage "MPPT3 Voltage" <energy> (mppt) ["Measurement", "Voltage"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt3-voltage"}
Number:ElectricCurrent mppt3_current "MPPT3 Current" <energy> (mppt) ["Measurement", "Current"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt3-current"}
Number:Power mppt3_power "MPPT3 Power" <line> (mppt) ["Measurement", "Power"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt3-power"}
Number:ElectricPotential mppt4_voltage "MPPT4 Voltage" <energy> (mppt) ["Measurement", "Voltage"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt4-voltage"}
Number:ElectricCurrent mppt4_current "MPPT4 Current" <energy> (mppt) ["Measurement", "Current"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt4-current"}
Number:Power mppt4_power "MPPT4 Power" <line> (mppt) ["Measurement", "Power"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-mppt-information#fi-mppt4-power"}
// Battery information
Number:Dimensionless battery_level "Battery Level [%.0f %%]" <battery> (batteryInformation,overview) ["Measurement", "Energy"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-level", unit="%"}
Number:Power battery_charging_power "Battery Charging Power" <line> (batteryInformation,overview) ["Measurement", "Power"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-charging-power"}
Number:ElectricPotential battery_voltage "Battery Voltage" <energy> (batteryInformation) ["Measurement", "Voltage"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-voltage"}
Number:ElectricCurrent battery_current "Battery Current" <energy> (batteryInformation) ["Measurement", "Current"]
{channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-current"}
Number:Temperature battery_temperature "Battery Temperature" <temperature> (overview) ["Measurement", "Temperature"]{channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-temperature"}
Number:Dimensionless battery_minimum_soc "Battery Min Charging Level [%.0f %%]" <battery> (batteryInformation) ["Control"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-minimum-soc", unit="%"}
Number:Dimensionless battery_maximum_soc "Battery Max Charging Level [%.0f %%]" <battery> (batteryInformation) ["Control"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-maximum-soc", unit="%"}
Number:Dimensionless battery_minimum_soc_on_grid "Battery Min Charging Level on Grid" <battery> (batteryInformation) ["Control"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-battery-information#fi-battery-minimum-soc-on-grid", unit="%"}
// Grid information
Number:ElectricPotential phase_a_voltage "Phase A Voltage" <energy> (gridInformation) ["Measurement"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-grid-information#fi-phase-a-voltage"}
Number:ElectricPotential phase_b_voltage "Phase B Voltage" <energy> (gridInformation) ["Measurement"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-grid-information#fi-phase-b-voltage"}
Number:ElectricPotential phase_c_voltage "Phase C Voltage" <energy> (gridInformation) ["Measurement"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-grid-information#fi-phase-c-voltage"}
Number:Frequency grid_frequency "Grid Frequency" <pump> (gridInformation) ["Measurement"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-grid-information#fi-grid-frequency"}
Number:Power grid_export_power "Export Power" <line> (gridInformation) ["Measurement", "Power"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-grid-information#fi-grid-export-power"}
Contact status_on_grid "Status Power Grid Available" (gridInformation) ["Measurement"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-grid-information#fi-status-on-grid"}
// EPS
Switch eps_output "EPS Output Enabled" (eps) ["Control"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-emergency-power-supply#fi-eps-output"}
Number:Power eps_export_power "EPS Export Power" <energy> (eps) ["Measurement", "Power"] {channel="modbus:mq2200-inverter:powerplant:inverter:fi-emergency-power-supply#fi-eps-export-power"}
```
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>5.1.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.modbus.foxinverter</artifactId>
<name>openHAB Add-ons :: Bundles :: Modbus FoxInverter Binding</name>
<dependencies>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.modbus</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,38 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import java.math.BigDecimal;
import java.math.BigInteger;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Constants for converting values.
*
* @author Sönke Küper - Initial contribution
* @author Holger Friedrich - Carry over from SunGrow binding, additions
*/
@NonNullByDefault
final class ConversionConstants {
private ConversionConstants() {
}
/**
* Multiplicand for 0.1.
*/
static final BigDecimal DIV_BY_TEN = new BigDecimal(BigInteger.ONE, 1);
static final BigDecimal DIV_BY_HUNDRED = new BigDecimal(BigInteger.ONE, 2);
static final BigDecimal DIV_BY_THOUSAND = new BigDecimal(BigInteger.ONE, 3);
}
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MQ2200InverterConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Holger Friedrich - Initial contribution
*/
@NonNullByDefault
public class MQ2200InverterConfiguration {
public int pollInterval;
public int maxTries;
}
@@ -0,0 +1,450 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import static org.openhab.binding.modbus.foxinverter.internal.ModbusFoxInverterBindingConstants.*;
import java.text.MessageFormat;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.modbus.handler.BaseModbusThingHandler;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
import org.openhab.core.io.transport.modbus.ModbusConstants;
import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
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.types.Command;
import org.openhab.core.types.RefreshType;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MQ2200InverterHandler} is responsible for reading the Modbus values of the
* FoxESS MQ-2200 inverter.
*
* @author Holger Friedrich - Initial contribution
*/
@NonNullByDefault
public class MQ2200InverterHandler extends BaseModbusThingHandler {
// used by Register definition to mark a new request must be created,
// required if you are not allowed to read certain registers ranges
public static final int ENFORCE_NEW_REQUEST = -1;
private static final int INVALID_ACTIVE_POWER_VALUE = -30000;
private static final int STATUS_BIT_STANDBY = 0x01;
private static final int STATUS_BIT_OPERATION = 0x04;
private static final int STATUS_BIT_FAULT = 0x40;
private static final int EPS_OUTPUT_ON_VALUE = 2;
private static final String HIDDEN_CHANNEL_PREFIX = "hidden-";
private static final String CHANNEL_ALARM1 = "alarm1";
private static final String CHANNEL_ALARM2 = "alarm2";
private static final String CHANNEL_ALARM3 = "alarm3";
private static final String CHANNEL_EPS_OUTPUT = "eps-output";
private static final String CHANNEL_GRID_FREQUENCY = "grid-frequency";
private static final String CHANNEL_INVERTER_POWER = "inverter-power";
private static final String CHANNEL_STATUS1 = "status1";
private static final String CHANNEL_STATUS_ALARM = "status-alarm";
private static final String CHANNEL_STATUS_ON_GRID = "status-on-grid";
private static final String CHANNEL_STATUS_OPERATION = "status-operation";
private static final String CHANNEL_STATUS_STANDBY = "status-standby";
private static final class ModbusRequest {
private final Deque<MQ2200InverterRegisters> registers;
private final ModbusReadRequestBlueprint blueprint;
public ModbusRequest(Deque<MQ2200InverterRegisters> registers, int slaveId, int tries) {
this.registers = registers;
this.blueprint = initReadRequest(registers, slaveId, tries);
}
private ModbusReadRequestBlueprint initReadRequest(Deque<MQ2200InverterRegisters> registers, int slaveId,
int tries) {
int firstRegister = Objects.requireNonNull(registers.getFirst()).getRegisterNumber();
int lastRegister = Objects.requireNonNull(registers.getLast()).getRegisterNumber();
int length = lastRegister - firstRegister + Objects.requireNonNull(registers.getLast()).getRegisterCount();
assert length <= ModbusConstants.MAX_REGISTERS_READ_COUNT;
return new ModbusReadRequestBlueprint(slaveId, ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS,
firstRegister, length, tries);
}
}
private final Logger logger = LoggerFactory.getLogger(MQ2200InverterHandler.class);
private LocaleProvider localeProvider;
private TranslationProvider translationProvider;
private final Bundle bundle;
private List<ModbusRequest> modbusRequests = new ArrayList<>();
private volatile int[] alarm = new int[3]; // cache status of the 3 alarm registers
private volatile boolean alarmState = false; // previous alarm state
private volatile boolean statusFault = false; // cache status of fault bit
public MQ2200InverterHandler(Thing thing, final TranslationProvider translationProvider,
final LocaleProvider localeProvider) {
super(thing);
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
bundle = FrameworkUtil.getBundle(this.getClass());
}
/**
* Splits the inverter Registers into multiple ModbusRequest, to ensure the max request size.
*/
private List<ModbusRequest> buildRequests(int tries) {
final List<ModbusRequest> requests = new ArrayList<>();
Deque<MQ2200InverterRegisters> currentRequest = new ArrayDeque<>();
int currentRequestFirstRegister = 0;
for (MQ2200InverterRegisters channel : MQ2200InverterRegisters.values()) {
logger.debug("Evaluating register {}", channel.name());
if (currentRequest.isEmpty()) {
currentRequest.add(channel);
currentRequestFirstRegister = channel.getRegisterNumber();
} else if (ENFORCE_NEW_REQUEST == channel.getRegisterNumber()) {
// marker, start new request
if (!currentRequest.isEmpty()) {
requests.add(new ModbusRequest(currentRequest, getSlaveId(), tries));
}
currentRequest = new ArrayDeque<>();
} else {
int sizeWithRegisterAdded = channel.getRegisterNumber() - currentRequestFirstRegister
+ channel.getRegisterCount();
if (sizeWithRegisterAdded > ModbusConstants.MAX_REGISTERS_READ_COUNT) {
requests.add(new ModbusRequest(currentRequest, getSlaveId(), tries));
currentRequest = new ArrayDeque<>();
currentRequest.add(channel);
currentRequestFirstRegister = channel.getRegisterNumber();
logger.debug("Starting new Modbus request template due to size limit, first register {} ({})",
channel.name(), currentRequestFirstRegister);
} else {
currentRequest.add(channel);
}
}
}
if (!currentRequest.isEmpty()) {
requests.add(new ModbusRequest(currentRequest, getSlaveId(), tries));
}
logger.debug("Created {} Modbus request templates.", requests.size());
return requests;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Inverter {}, channel {} received command {}", getThing().getUID(), channelUID, command);
if (command instanceof RefreshType && !this.modbusRequests.isEmpty()) {
logger.info("REFRESH command received, submitting one-time polls for all registers.");
readStaticData();
for (ModbusRequest request : this.modbusRequests) {
submitOneTimePoll(request.blueprint,
(AsyncModbusReadResult result) -> this.readSuccessful(request, result), this::readError);
}
} else {
// TODO implement sending commands to device, maybe safeguarded by checking the model info first
logger.warn("Command {} on channel {} is not supported yet.", command, channelUID);
}
}
@Override
public void modbusInitialize() {
final MQ2200InverterConfiguration config = getConfigAs(MQ2200InverterConfiguration.class);
if (config.pollInterval <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
getTranslation("offline.config.poll_interval", config.pollInterval));
return;
}
if (config.maxTries <= 0) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
getTranslation("offline.config.max_tries", config.maxTries));
return;
}
this.updateStatus(ThingStatus.UNKNOWN);
// read static data, store into properties
getThing().setProperties(readStaticData());
// setup regular polling
this.modbusRequests = this.buildRequests(config.maxTries);
for (ModbusRequest request : modbusRequests) {
registerRegularPoll(request.blueprint, config.pollInterval, 0,
(AsyncModbusReadResult result) -> this.readSuccessful(request, result), this::readError);
}
}
private Map<String, String> readStaticData() {
Map<String, String> properties = new java.util.concurrent.ConcurrentHashMap<>();
try {
// Initial poll of static info
// a) manufacturer, model, serial number
submitOneTimePoll(new ModbusReadRequestBlueprint(getSlaveId(),
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 30000, 16 * 3, 3),
(AsyncModbusReadResult result) -> {
logger.trace("Initial poll successful {}", result);
byte[] res = result.getRegisters().get().getBytes();
String modelInfo = new String(res, 0, 16).trim();
String serialNo = new String(res, 16, 16).trim();
String manufacturerId = new String(res, 32, 16).trim();
logger.debug("Inverter Model: {}, S/N: {}, Manufacturer ID: {}", modelInfo, serialNo,
manufacturerId);
if (!modelInfo.isEmpty()) {
properties.put(PROPERTY_MODEL_NAME, modelInfo);
}
if (!serialNo.isEmpty()) {
properties.put(PROPERTY_SERIAL_NO, serialNo);
}
if (!manufacturerId.isEmpty()) {
properties.put(PROPERTY_MANUFACTURER_ID, manufacturerId);
}
}, (AsyncModbusFailure<ModbusReadRequestBlueprint> error) -> {
logger.warn("Initial read of model information failed", error.getCause());
});
Thread.sleep(100);
// b) firmware versions
submitOneTimePoll(new ModbusReadRequestBlueprint(getSlaveId(),
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 36001, 3, 3), (AsyncModbusReadResult result) -> {
byte[] res = result.getRegisters().get().getBytes();
properties.put(PROPERTY_FIRMWARE_WR, String.format("%d.%03d", res[0], res[1]));
// TODO The mapping for PROPERTY_FIRMWARE_PV is not confirmed, PV FW could also be stored in
// res[4,5]
properties.put(PROPERTY_FIRMWARE_PV, String.format("%d.%03d", res[2], res[3]));
}, (AsyncModbusFailure<ModbusReadRequestBlueprint> error) -> {
// reading properties is not critical, just log at debug level
logger.debug("Reading firmware WR/PV failed", error.getCause());
});
Thread.sleep(100);
// c) firmware versions
submitOneTimePoll(new ModbusReadRequestBlueprint(getSlaveId(),
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 37003, 1, 3), (AsyncModbusReadResult result) -> {
byte[] res = result.getRegisters().get().getBytes();
properties.put(PROPERTY_FIRMWARE_BMS, String.format("%d.%03d", res[0], res[1]));
}, (AsyncModbusFailure<ModbusReadRequestBlueprint> error) -> {
// reading properties is not critical, just log at debug level
logger.debug("Reading firmware BMS failed", error.getCause());
});
Thread.sleep(100);
// d) rated power, max active power
submitOneTimePoll(new ModbusReadRequestBlueprint(getSlaveId(),
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, 39053, 2 * 2, 3),
(AsyncModbusReadResult result) -> {
ModbusBitUtilities.extractStateFromRegisters(result.getRegisters().get(), 0,
ModbusConstants.ValueType.UINT32).ifPresent(v -> {
properties.put(PROPERTY_RATED_POWER, Objects.toString(v.toBigDecimal()) + " W");
});
ModbusBitUtilities.extractStateFromRegisters(result.getRegisters().get(), 2,
ModbusConstants.ValueType.UINT32).ifPresent(v -> {
properties.put(PROPERTY_MAX_ACTIVE_POWER,
Objects.toString(v.toBigDecimal()) + " W");
});
}, (AsyncModbusFailure<ModbusReadRequestBlueprint> error) -> {
// reading properties is not critical, just log at debug level
logger.debug("Reading rated/max active power failed", error.getCause());
});
// wait for async polls to complete; waiting is not ideal but not worth implementing a complex logic here
Thread.sleep(1000);
} catch (InterruptedException e) {
logger.debug("Interrupted while reading device properties");
Thread.currentThread().interrupt();
}
return properties;
}
private void processAlarmState() {
boolean currentAlarmState = (alarm[0] != 0) || (alarm[1] != 0) || (alarm[2] != 0) || statusFault;
if (currentAlarmState != alarmState) {
alarmState = currentAlarmState;
if (alarmState) {
logger.warn("Inverter {} is in Alarm State: Alarm1=0x{}, Alarm2=0x{}, Alarm3=0x{}, StatusFault={}",
getThing().getUID(), Integer.toHexString(alarm[0]), Integer.toHexString(alarm[1]),
Integer.toHexString(alarm[2]), statusFault);
// indicate severe error also by setting the ThingStatus
// (UNKNOWN seems more appropriate than OFFLINE, as the device still gets commands)
updateStatus(ThingStatus.UNKNOWN);
} else {
logger.info("Inverter {}, alarm resolved", getThing().getUID());
updateStatus(ThingStatus.ONLINE);
}
}
OpenClosedType state = alarmState ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
logger.debug("{} {} -> {}", CHANNEL_STATUS_ALARM.toUpperCase(), alarmState, state);
updateState(new ChannelUID(thing.getUID(), "fi-overview", "fi-" + CHANNEL_STATUS_ALARM), state);
}
private void processHiddenChannel(MQ2200InverterRegisters channel, org.openhab.core.types.State v) {
// this block deals with channels which are not directly exposed, but
// used to update other channels
logger.trace("Update on internal channel {} to {}", channel.getChannelName(), v);
DecimalType d = v.as(DecimalType.class);
if (d == null) {
logger.warn("Internal channel {} is not DecimalType, cannot process", channel.getChannelName());
} else {
int i = d.intValue();
switch (channel.getChannelName().substring(HIDDEN_CHANNEL_PREFIX.length())) {
// active power reports -30000 W during startup, to be suppressed
case CHANNEL_INVERTER_POWER:
if (i != INVALID_ACTIVE_POWER_VALUE) {
logger.debug("{} {}", CHANNEL_INVERTER_POWER.toUpperCase(), v);
updateState(new ChannelUID(thing.getUID(), "fi-" + channel.getChannelGroup(),
"fi-" + CHANNEL_INVERTER_POWER), v);
}
break;
// grid frequency is used to create STATUS_ON_GRID
case CHANNEL_GRID_FREQUENCY:
logger.debug("{} {}", CHANNEL_GRID_FREQUENCY.toUpperCase(), v);
updateState(new ChannelUID(thing.getUID(), "fi-" + channel.getChannelGroup(),
"fi-" + CHANNEL_GRID_FREQUENCY), v);
OpenClosedType state = (i >= 1) ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
logger.debug("{} {} -> {}", CHANNEL_STATUS_ON_GRID.toUpperCase(), i >= 1, state);
updateState(new ChannelUID(thing.getUID(), "fi-" + channel.getChannelGroup(),
"fi-" + CHANNEL_STATUS_ON_GRID), state);
break;
// status seems to use only 3 bits, export a separate channel for each
case CHANNEL_STATUS1:
boolean statusStandby = (i & STATUS_BIT_STANDBY) != 0;
boolean statusOperation = (i & STATUS_BIT_OPERATION) != 0;
OpenClosedType stateUpdate = statusStandby ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
logger.debug("{} {} -> {}", CHANNEL_STATUS_STANDBY.toUpperCase(), statusStandby, stateUpdate);
updateState(new ChannelUID(thing.getUID(), "fi-" + channel.getChannelGroup(),
"fi-" + CHANNEL_STATUS_STANDBY), stateUpdate);
stateUpdate = statusOperation ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
logger.debug("{} {} -> {}", CHANNEL_STATUS_OPERATION.toUpperCase(), statusOperation, stateUpdate);
updateState(new ChannelUID(thing.getUID(), "fi-" + channel.getChannelGroup(),
"fi-" + CHANNEL_STATUS_OPERATION), stateUpdate);
// this is a global variable, as the fault state is stored and evaluated for alarm output
statusFault = (i & STATUS_BIT_FAULT) != 0;
processAlarmState();
break;
// alarm states are currently
case CHANNEL_ALARM1:
alarm[0] = i;
processAlarmState();
break;
case CHANNEL_ALARM2:
alarm[1] = i;
processAlarmState();
break;
case CHANNEL_ALARM3:
alarm[2] = i;
processAlarmState();
break;
// EPS output state is set to 0 or 2 by the app
case CHANNEL_EPS_OUTPUT:
OnOffType epsState = OnOffType.OFF;
if (i == EPS_OUTPUT_ON_VALUE) {
epsState = OnOffType.ON;
}
logger.debug("{} {}", CHANNEL_EPS_OUTPUT.toUpperCase(), epsState);
updateState(new ChannelUID(thing.getUID(), "fi-" + channel.getChannelGroup(),
"fi-" + CHANNEL_EPS_OUTPUT), epsState);
break;
default:
logger.warn("Unhandled internal channel {}", channel.getChannelName());
}
}
}
private void readSuccessful(ModbusRequest request, AsyncModbusReadResult result) {
logger.trace("readSuccessful {}: {}", request, result);
result.getRegisters().ifPresent(registers -> {
if (getThing().getStatus() != ThingStatus.ONLINE) {
// if alarm is set, do not set ONLINE as is would override the alarm indication
if (!alarmState) {
updateStatus(ThingStatus.ONLINE);
}
}
int firstRegister = Objects.requireNonNull(request.registers.getFirst()).getRegisterNumber();
for (MQ2200InverterRegisters channel : request.registers) {
int index = channel.getRegisterNumber() - firstRegister;
logger.debug("{} {}", channel.toString(), ModbusBitUtilities
.extractStateFromRegisters(registers, index, channel.getType()).map(channel::createState));
ModbusBitUtilities.extractStateFromRegisters(registers, index, channel.getType())
.map(channel::createState).ifPresentOrElse(v -> {
if (!channel.getChannelName().startsWith(HIDDEN_CHANNEL_PREFIX)) {
updateState(createChannelUid(channel), v);
} else {
processHiddenChannel(channel, v);
}
}, () -> {
logger.warn("Could not extract state for channel {}", channel.getChannelName());
});
}
});
}
private void readError(AsyncModbusFailure<ModbusReadRequestBlueprint> error) {
// TODO improve error handling, stop regular polling on repeated errors, sporadic polling only
this.logger.debug("Failed to get Modbus data", error.getCause());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
getTranslation("offline.communication_error", error.getCause().getMessage()));
}
private ChannelUID createChannelUid(MQ2200InverterRegisters register) {
return new ChannelUID(thing.getUID(), "fi-" + register.getChannelGroup(), "fi-" + register.getChannelName());
}
/**
* get translated text
*
* @param text text to be translated, may contain placeholders {n} for the n-th optional argument of this function
* @param arguments any optional arguments, will be inserted
* @return translated text with substitutions if translationProvider is set and provides a translation, otherwise
* returns original text with substitutions
*/
public String getTranslation(final String text, @Nullable Object @Nullable... arguments) {
// locale cannot be cached, as getLocale() will return different result once locale is changed by user
final Locale locale = localeProvider.getLocale();
final String res = translationProvider.getText(bundle, text, text, locale, arguments);
if (res != null) {
return res;
}
// translating not possible, we still have the original text without any substitutions
if (arguments == null || arguments.length == 0) {
return text;
}
// else execute pattern substitution in untranslated text
return MessageFormat.format(text, arguments);
}
}
@@ -0,0 +1,234 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import static org.openhab.core.io.transport.modbus.ModbusConstants.ValueType.*;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.function.Function;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.io.transport.modbus.ModbusConstants.ValueType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State;
/**
* The {@link MQ2200InverterRegisters} is responsible for defining Modbus registers and their units.
*
* @author Holger Friedrich - Initial contribution
*/
@NonNullByDefault
public enum MQ2200InverterRegisters {
// this temperature is shown in the app
BATTERY_TEMPERATURE(37617, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(SIUnits.CELSIUS),
"battery-information"),
METER_CONNECTED(38801, UINT16, BigDecimal.ONE, contactFactory(), "overview"),
// seems not useful yet, always 0
// Thing data protocolVersion "Protocol version" [ readStart="39000", readValueType="uint32" ] // always 0
// status is 1 during start and shutdown, 4 during normal operation, 0x40 on fault
// (poweroff cannot be read, as it will shut down Modbus communication)
HIDDEN_STATUS1(39063, UINT16, BigDecimal.ONE, DecimalType::new, "overview"),
// STATUS3 seems to use only bit0, but it does not indicate the grid status properly
// grid outage could be detected via GRID_FREQUENCY register
// STATUS_ON_GRID(39065, UINT32, BigDecimal.ONE, contactFactory(), "status"),
HIDDEN_ALARM1(39067, UINT16, BigDecimal.ONE, DecimalType::new, ""),
HIDDEN_ALARM2(39068, UINT16, BigDecimal.ONE, DecimalType::new, ""),
HIDDEN_ALARM3(39069, UINT16, BigDecimal.ONE, DecimalType::new, ""),
// NOTE: this list needs to be sorted by register number!
MPPT1_VOLTAGE(39070, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "mppt-information"),
MPPT1_CURRENT(39071, UINT16, ConversionConstants.DIV_BY_HUNDRED, quantityFactory(Units.AMPERE), "mppt-information"),
MPPT2_VOLTAGE(39072, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "mppt-information"),
MPPT2_CURRENT(39073, UINT16, ConversionConstants.DIV_BY_HUNDRED, quantityFactory(Units.AMPERE), "mppt-information"),
MPPT3_VOLTAGE(39074, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "mppt-information"),
MPPT3_CURRENT(39075, UINT16, ConversionConstants.DIV_BY_HUNDRED, quantityFactory(Units.AMPERE), "mppt-information"),
MPPT4_VOLTAGE(39076, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "mppt-information"),
MPPT4_CURRENT(39077, UINT16, ConversionConstants.DIV_BY_HUNDRED, quantityFactory(Units.AMPERE), "mppt-information"),
PV_POWER(39118, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "mppt-information"),
HIDDEN_GRID_FREQUENCY(39139, UINT16, new BigDecimal(BigInteger.ONE, 2), quantityFactory(Units.HERTZ),
"grid-information"),
PHASE_A_VOLTAGE(39123, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "grid-information"),
PHASE_B_VOLTAGE(39124, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "grid-information"),
PHASE_C_VOLTAGE(39125, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "grid-information"),
// INVERTER_x_CURRENT 39126, 39128, 39130 seem not to work for MQ2200
HIDDEN_INVERTER_POWER(39134, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "overview"),
// seems not to work for MQ2200
// REACTIVE_POWER(39136, INT32, BigDecimal.ONE, quantityFactory(Units.VAR), "overview"),
// POWER_FACTOR(39138, INT16, ConversionConstants.DIV_BY_THOUSAND, DecimalType::new, "overview"),
INTERNAL_TEMPERATURE(39141, INT16, ConversionConstants.DIV_BY_TEN, quantityFactory(SIUnits.CELSIUS), "overview"),
TOTAL_PV_GENERATION(39149, UINT32, ConversionConstants.DIV_BY_HUNDRED, quantityFactory(Units.KILOWATT_HOUR),
"overview"),
DAILY_PV_GENERATION(39151, UINT32, ConversionConstants.DIV_BY_HUNDRED, quantityFactory(Units.KILOWATT_HOUR),
"overview"),
HOME_IMPORT_POWER(39162, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "overview"),
GRID_EXPORT_POWER(39168, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "grid-information"),
EPS_EXPORT_POWER(39216, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "emergency-power-supply"),
BATTERY_VOLTAGE(39227, UINT16, ConversionConstants.DIV_BY_TEN, quantityFactory(Units.VOLT), "battery-information"),
BATTERY_CURRENT(39228, INT32, ConversionConstants.DIV_BY_THOUSAND, quantityFactory(Units.AMPERE),
"battery-information"),
// prefer combined power over bat1 power
BATTERY_CHARGING_POWER(39237, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "battery-information"),
MPPT1_POWER(39279, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "mppt-information"),
MPPT2_POWER(39281, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "mppt-information"),
MPPT3_POWER(39283, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "mppt-information"),
MPPT4_POWER(39285, INT32, BigDecimal.ONE, quantityFactory(Units.WATT), "mppt-information"),
BATTERY_LEVEL(39424, UINT16, BigDecimal.ONE, percentFactory(), "battery-information"),
// TODO implement remote control, registers 46001-46007
BATTERY_MINIMUM_SOC(46609, UINT16, BigDecimal.ONE, percentFactory(), "battery-information"),
BATTERY_MAXIMUM_SOC(46610, UINT16, BigDecimal.ONE, percentFactory(), "battery-information"),
BATTERY_MINIMUM_SOC_ON_GRID(46611, UINT16, BigDecimal.ONE, percentFactory(), "battery-information"),
// 0:off 2:on, special handling in MQ2200InverterHandler
HIDDEN_EPS_OUTPUT(46613, UINT16, BigDecimal.ONE, DecimalType::new, "emergency-power-supply");
// does not work, always returns 7
// WORK_MODE(49203, UINT16, BigDecimal.ONE, DecimalType::new, "overview"),
// some registers in between cannot be read and will make the Modbus request fail
// BLOCKER(MQ2200InverterHandler.ENFORCE_NEW_REQUEST, UINT16, BigDecimal.ONE, DecimalType::new, ""),
// does not change during manual shutdown using the button on the device
// SYSTEM_POWER_STATE(49228, UINT16, BigDecimal.ONE, DecimalType::new, "overview"),
// idle state seems to be 0
// IDLE_STATE(49229, UINT16, BigDecimal.ONE, DecimalType::new, "overview"),
// IDLE_LOAD_POWER_THRESHOLD(49230, UINT16, BigDecimal.ONE, DecimalType::new, "overview");
private final BigDecimal multiplier;
private final int registerNumber;
private final ValueType type;
private final Function<BigDecimal, BigDecimal> conversion;
private final Function<BigDecimal, State> stateFactory;
private final String channelGroup;
MQ2200InverterRegisters(int registerNumber, ValueType type, BigDecimal multiplier,
Function<BigDecimal, State> stateFactory, Function<BigDecimal, BigDecimal> conversion,
String channelGroup) {
this.multiplier = multiplier;
this.registerNumber = registerNumber;
this.type = type;
this.conversion = conversion;
this.stateFactory = stateFactory;
this.channelGroup = channelGroup;
}
MQ2200InverterRegisters(int registerNumber, ValueType type, BigDecimal multiplier,
Function<BigDecimal, State> stateFactory, String channelGroup) {
this.multiplier = multiplier;
this.registerNumber = registerNumber;
this.type = type;
this.conversion = Function.identity();
this.stateFactory = stateFactory;
this.channelGroup = channelGroup;
}
private static Function<BigDecimal, State> contactFactory() {
return (BigDecimal value) -> value.intValue() == 0 ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
}
private static Function<BigDecimal, State> percentFactory() {
return (BigDecimal value) -> new QuantityType<>(value, Units.PERCENT);
}
/**
* Creates a Function that creates {@link QuantityType} states with the given {@link Unit}.
*
* @param unit {@link Unit} to be used for the value.
* @return Function for value creation.
*/
private static Function<BigDecimal, State> quantityFactory(Unit<?> unit) {
return (BigDecimal value) -> new QuantityType<>(value, unit);
}
/**
* Returns the Modbus register number.
*
* @return Modbus register number.
*/
public int getRegisterNumber() {
return registerNumber;
}
/**
* Returns the {@link ValueType} for the channel.
*
* @return {@link ValueType} for the channel.
*/
public ValueType getType() {
return type;
}
/**
* Returns the count of registers read to return the value of this register.
*
* @return register count.
*/
public int getRegisterCount() {
return this.type.getBits() / 16;
}
/**
* Returns the channel group.
*
* @return channel group id.
*/
public String getChannelGroup() {
return channelGroup;
}
/**
* Returns the channel name.
*
* @return the channel name.
*/
public String getChannelName() {
return this.name().toLowerCase().replace('_', '-');
}
/**
* Creates the {@link State} for the given register value.
*
* @param registerValue the value for the channel.
* @return {@link State} for the given value.
*/
public State createState(DecimalType registerValue) {
final BigDecimal scaledValue = registerValue.toBigDecimal().multiply(this.multiplier);
final BigDecimal convertedValue = conversion.apply(scaledValue);
return this.stateFactory.apply(convertedValue);
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.modbus.ModbusBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link ModbusFoxInverterBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Holger Friedrich - Initial contribution
*/
@NonNullByDefault
public class ModbusFoxInverterBindingConstants {
public static final String PROPERTY_MODEL_NAME = "modelName";
public static final String PROPERTY_SERIAL_NO = "serialNumber";
public static final String PROPERTY_MANUFACTURER_ID = "manufacturerId";
public static final String PROPERTY_RATED_POWER = "ratedPower";
public static final String PROPERTY_MAX_ACTIVE_POWER = "maxActivePower";
public static final String PROPERTY_FIRMWARE_BMS = "firmwareBMS";
public static final String PROPERTY_FIRMWARE_WR = "firmwareWR";
public static final String PROPERTY_FIRMWARE_PV = "firmwarePV";
/**
* ThingType-ID for Inverter.
*/
public static final ThingTypeUID THING_TYPE_MQ2200_INVERTER = new ThingTypeUID(ModbusBindingConstants.BINDING_ID,
"mq2200-inverter");
}
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import static org.openhab.binding.modbus.foxinverter.internal.ModbusFoxInverterBindingConstants.THING_TYPE_MQ2200_INVERTER;
import java.util.HashMap;
import java.util.Map;
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.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.io.transport.modbus.AsyncModbusFailure;
import org.openhab.core.io.transport.modbus.AsyncModbusReadResult;
import org.openhab.core.io.transport.modbus.ModbusCommunicationInterface;
import org.openhab.core.io.transport.modbus.ModbusReadFunctionCode;
import org.openhab.core.io.transport.modbus.ModbusReadRequestBlueprint;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Discovery service for FoxInverter devices
*
* @author Holger Friedrich - initial contribution
*
*/
@Component
@NonNullByDefault
public class ModbusFoxInverterDiscoveryParticipant implements ModbusDiscoveryParticipant {
private final Logger logger = LoggerFactory.getLogger(ModbusFoxInverterDiscoveryParticipant.class);
private class FoxInverterDiscoveryProcess {
private static final String PROPERTY_UNIQUE_ADDRESS = "uniqueAddress";
private static final String MQ2200_IDENTIFIER = "MQ2200";
private static final int MQ2200_START_ADDRESS = 30000;
private final ModbusEndpointThingHandler handler;
private final ModbusDiscoveryListener listener;
public FoxInverterDiscoveryProcess(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener) {
this.handler = handler;
this.listener = listener;
}
public void detectModel() throws EndpointNotInitializedException {
logger.debug("Beginning scan for FoxESS MQ-2200 device at address {}", MQ2200_START_ADDRESS);
ModbusCommunicationInterface comms = handler.getCommunicationInterface();
if (comms == null) {
throw new EndpointNotInitializedException();
}
// Initial poll of static info
// manufacturer, model, serial number
comms.submitOneTimePoll(new ModbusReadRequestBlueprint(handler.getSlaveId(),
ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, MQ2200_START_ADDRESS, 16 * 3, 3),
(AsyncModbusReadResult result) -> {
logger.trace("Initial poll successful {}", result);
byte[] res = result.getRegisters().get().getBytes();
String modelInfo = new String(res, 0, 16).trim();
if (modelInfo.startsWith(MQ2200_IDENTIFIER)) {
// Handle MQ2200 specific logic
String serialNo = new String(res, 16, 16).trim();
String manufacturerId = new String(res, 32, 16).trim();
logger.debug("Detected Inverter Model: {}, S/N: {}, Manufacturer ID: {}", modelInfo,
serialNo, manufacturerId);
// construct an ID, only alphanumeric characters, lower case
String deviceId = (serialNo + manufacturerId).replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
if (deviceId.isBlank()) {
logger.debug("Device ID is blank, cannot create discovery result");
} else {
ThingTypeUID thingTypeUID = THING_TYPE_MQ2200_INVERTER;
ThingUID thingUID = new ThingUID(thingTypeUID, handler.getUID(), deviceId);
Map<String, Object> properties = new HashMap<>();
properties.put(PROPERTY_UNIQUE_ADDRESS,
handler.getUID().getAsString() + ":" + deviceId);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID)
.withProperties(properties).withRepresentationProperty(PROPERTY_UNIQUE_ADDRESS)
.withBridge(handler.getUID()).withLabel("FoxESS MQ-2200 Inverter").build();
listener.thingDiscovered(discoveryResult);
}
}
listener.discoveryFinished();
}, (AsyncModbusFailure<ModbusReadRequestBlueprint> error) -> {
logger.debug("Reading device info from MQ2200 failed, skipping detection", error.getCause());
listener.discoveryFinished();
});
}
}
@Override
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
return Set.of(THING_TYPE_MQ2200_INVERTER);
}
@Override
public void startDiscovery(ModbusEndpointThingHandler handler, ModbusDiscoveryListener listener) {
logger.debug("Starting fox inverter discovery");
try {
new FoxInverterDiscoveryProcess(handler, listener).detectModel();
} catch (EndpointNotInitializedException ex) {
logger.debug("Could not start discovery process");
listener.discoveryFinished();
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import static org.openhab.binding.modbus.foxinverter.internal.ModbusFoxInverterBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
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.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link ModbusFoxInverterHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Holger Friedrich - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.foxinverter", service = ThingHandlerFactory.class)
public class ModbusFoxInverterHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_MQ2200_INVERTER);
private LocaleProvider localeProvider;
private TranslationProvider translationProvider;
@Activate
public ModbusFoxInverterHandlerFactory(final @Reference TranslationProvider translationProvider,
final @Reference LocaleProvider localeProvider) {
this.translationProvider = translationProvider;
this.localeProvider = localeProvider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_MQ2200_INVERTER.equals(thingTypeUID)) {
return new MQ2200InverterHandler(thing, translationProvider, localeProvider);
}
return null;
}
}
@@ -0,0 +1,22 @@
<?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:foxinverter:mq2200-inverter">
<parameter name="pollInterval" type="integer" required="true" min="100" unit="ms">
<label>Poll Interval</label>
<description>Time between polling the data in ms.</description>
<default>5000</default>
</parameter>
<parameter name="maxTries" type="integer" min="1">
<label>Maximum Tries When Reading</label>
<default>3</default>
<description>Specifies how many times the binding should retry reading data if a read attempt fails.
Set to `1` to
disable retries and use a single attempt.</description>
</parameter>
</config-description>
</config-description:config-descriptions>
@@ -0,0 +1,71 @@
# thing types
thing-type.modbus.mq2200-inverter.label = FoxESS MQ-2200 Inverter
thing-type.modbus.mq2200-inverter.description = FoxESS MQ-2200 / Solakon ONE, Avocado 22 Pro inverter connected via Modbus TCP.
# thing types config
thing-type.config.foxinverter.mq2200-inverter.maxTries.label = Maximum Tries When Reading
thing-type.config.foxinverter.mq2200-inverter.maxTries.description = Specifies how many times the binding should retry reading data if a read attempt fails. Set to `1` to disable retries and use a single attempt.
thing-type.config.foxinverter.mq2200-inverter.pollInterval.label = Poll Interval
thing-type.config.foxinverter.mq2200-inverter.pollInterval.description = Time between polling the data in ms.
# channel group types
channel-group-type.modbus.fi-battery-information.label = Battery Information
channel-group-type.modbus.fi-emergency-power-supply.label = EPS Information
channel-group-type.modbus.fi-grid-information.label = Grid Information
channel-group-type.modbus.fi-mppt-information.label = MPPT Information
channel-group-type.modbus.fi-overview.label = Overview
# channel types
channel-type.modbus.fi-battery-charging-power.label = Battery Charging Power
channel-type.modbus.fi-battery-charging-power.description = Battery power flow. Positive values indicate charging, negative values indicate discharging.
channel-type.modbus.fi-battery-current.label = Battery Current
channel-type.modbus.fi-battery-level.label = Battery Level
channel-type.modbus.fi-battery-maximum-soc.label = Battery maximum state of charge
channel-type.modbus.fi-battery-minimum-soc-on-grid.label = Battery minimum state of charge (on grid)
channel-type.modbus.fi-battery-minimum-soc.label = Battery minimum state of charge
channel-type.modbus.fi-battery-voltage.label = Battery Voltage
channel-type.modbus.fi-daily-pv-generation.label = Daily PV Generation
channel-type.modbus.fi-eps-export-power.label = EPS Export Power
channel-type.modbus.fi-eps-export-power.description = Emergency power supply socket power. Positive values indicate supplying connected devices, negative values indicate receiving power from external source.
channel-type.modbus.fi-eps-output.label = EPS Enabled
channel-type.modbus.fi-grid-export-power.label = Grid Export Power
channel-type.modbus.fi-grid-export-power.description = Power flow with public grid. Positive values indicate export to grid, negative values indicate import from grid.
channel-type.modbus.fi-grid-frequency.label = Grid Frequency
channel-type.modbus.fi-home-import-power.label = Home Import Power
channel-type.modbus.fi-home-import-power.description = Power flow with home network. Negative when feeding home, positive when drawing from grid.
channel-type.modbus.fi-internal-temperature.label = Internal Temperature
channel-type.modbus.fi-inverter-power.label = Inverter Power
channel-type.modbus.fi-inverter-power.description = Inverter active power output. Always positive.
channel-type.modbus.fi-meter-connected.label = Meter Connected
channel-type.modbus.fi-mppt1-current.label = MPPT1 Current
channel-type.modbus.fi-mppt1-power.label = MPPT1 Power
channel-type.modbus.fi-mppt1-voltage.label = MPPT1 Voltage
channel-type.modbus.fi-mppt2-current.label = MPPT2 Current
channel-type.modbus.fi-mppt2-power.label = MPPT2 Power
channel-type.modbus.fi-mppt2-voltage.label = MPPT2 Voltage
channel-type.modbus.fi-mppt3-current.label = MPPT3 Current
channel-type.modbus.fi-mppt3-power.label = MPPT3 Power
channel-type.modbus.fi-mppt3-voltage.label = MPPT3 Voltage
channel-type.modbus.fi-mppt4-current.label = MPPT4 Current
channel-type.modbus.fi-mppt4-power.label = MPPT4 Power
channel-type.modbus.fi-mppt4-voltage.label = MPPT4 Voltage
channel-type.modbus.fi-phase-a-voltage.label = Phase A Voltage
channel-type.modbus.fi-phase-b-voltage.label = Phase B Voltage
channel-type.modbus.fi-phase-c-voltage.label = Phase C Voltage
channel-type.modbus.fi-pv-power.label = PV Power
channel-type.modbus.fi-pv-power.description = Current solar power generation from PV panels. Always positive.
channel-type.modbus.fi-status-alarm.label = System Status: Alarm
channel-type.modbus.fi-status-on-grid.label = On Grid
channel-type.modbus.fi-status-operation.label = System Status: Operation
channel-type.modbus.fi-status-standby.label = System Status: Standby
channel-type.modbus.fi-total-pv-generation.label = Total PV Generation
# specific strings
offline.communication_error = Failed to retrieve data: {0}
offline.config.poll_interval = Invalid poll interval: {0}
offline.config.max_tries = Invalid Maximum Tries When Reading: {0}
@@ -0,0 +1,476 @@
<?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="mq2200-inverter">
<supported-bridge-type-refs>
<bridge-type-ref id="tcp"/>
</supported-bridge-type-refs>
<label>FoxESS MQ-2200 Inverter</label>
<description>FoxESS MQ-2200 / Solakon ONE, Avocado 22 Pro inverter connected via Modbus TCP.</description>
<category>Inverter</category>
<channel-groups>
<channel-group id="fi-overview" typeId="fi-overview"/>
<channel-group id="fi-mppt-information" typeId="fi-mppt-information"/>
<channel-group id="fi-battery-information" typeId="fi-battery-information"/>
<channel-group id="fi-grid-information" typeId="fi-grid-information"/>
<channel-group id="fi-emergency-power-supply" typeId="fi-emergency-power-supply"/>
</channel-groups>
<config-description-ref uri="thing-type:foxinverter:mq2200-inverter"/>
</thing-type>
<channel-group-type id="fi-overview">
<label>Overview</label>
<channels>
<channel id="fi-inverter-power" typeId="fi-inverter-power"/>
<channel id="fi-home-import-power" typeId="fi-home-import-power"/>
<channel id="fi-daily-pv-generation" typeId="fi-daily-pv-generation"/>
<channel id="fi-total-pv-generation" typeId="fi-total-pv-generation"/>
<channel id="fi-internal-temperature" typeId="fi-internal-temperature"/>
<channel id="fi-meter-connected" typeId="fi-meter-connected"/>
<channel id="fi-status-alarm" typeId="fi-status-alarm"/>
<channel id="fi-status-operation" typeId="fi-status-operation"/>
<channel id="fi-status-standby" typeId="fi-status-standby"/>
</channels>
</channel-group-type>
<channel-group-type id="fi-mppt-information">
<label>MPPT Information</label>
<channels>
<channel id="fi-pv-power" typeId="fi-pv-power"/>
<channel id="fi-mppt1-voltage" typeId="fi-mppt1-voltage"/>
<channel id="fi-mppt1-current" typeId="fi-mppt1-current"/>
<channel id="fi-mppt1-power" typeId="fi-mppt1-power"/>
<channel id="fi-mppt2-voltage" typeId="fi-mppt2-voltage"/>
<channel id="fi-mppt2-current" typeId="fi-mppt2-current"/>
<channel id="fi-mppt2-power" typeId="fi-mppt2-power"/>
<channel id="fi-mppt3-voltage" typeId="fi-mppt3-voltage"/>
<channel id="fi-mppt3-current" typeId="fi-mppt3-current"/>
<channel id="fi-mppt3-power" typeId="fi-mppt3-power"/>
<channel id="fi-mppt4-voltage" typeId="fi-mppt4-voltage"/>
<channel id="fi-mppt4-current" typeId="fi-mppt4-current"/>
<channel id="fi-mppt4-power" typeId="fi-mppt4-power"/>
</channels>
</channel-group-type>
<channel-group-type id="fi-battery-information">
<label>Battery Information</label>
<category>Battery</category>
<channels>
<channel id="fi-battery-voltage" typeId="fi-battery-voltage"/>
<channel id="fi-battery-current" typeId="fi-battery-current"/>
<channel id="fi-battery-level" typeId="fi-battery-level"/>
<channel id="fi-battery-charging-power" typeId="fi-battery-charging-power"/>
<channel id="fi-battery-temperature" typeId="fi-battery-temperature"/>
<channel id="fi-battery-minimum-soc" typeId="fi-battery-minimum-soc"/>
<channel id="fi-battery-maximum-soc" typeId="fi-battery-maximum-soc"/>
<channel id="fi-battery-minimum-soc-on-grid" typeId="fi-battery-minimum-soc-on-grid"/>
</channels>
</channel-group-type>
<channel-group-type id="fi-grid-information">
<label>Grid Information</label>
<channels>
<channel id="fi-phase-a-voltage" typeId="fi-phase-a-voltage"/>
<channel id="fi-phase-b-voltage" typeId="fi-phase-b-voltage"/>
<channel id="fi-phase-c-voltage" typeId="fi-phase-c-voltage"/>
<channel id="fi-grid-frequency" typeId="fi-grid-frequency"/>
<channel id="fi-grid-export-power" typeId="fi-grid-export-power"/>
<channel id="fi-status-on-grid" typeId="fi-status-on-grid"/>
</channels>
</channel-group-type>
<channel-group-type id="fi-emergency-power-supply">
<label>EPS Information</label>
<channels>
<channel id="fi-eps-output" typeId="fi-eps-output"/>
<channel id="fi-eps-export-power" typeId="fi-eps-export-power"/>
</channels>
</channel-group-type>
<channel-type id="fi-inverter-power">
<item-type>Number:Power</item-type>
<label>Inverter Power</label>
<description>Inverter active power output. Always positive.</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-home-import-power">
<item-type>Number:Power</item-type>
<label>Home Import Power</label>
<description>Power flow with home network. Negative when feeding home, positive when drawing from grid.</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-daily-pv-generation">
<item-type>Number:Energy</item-type>
<label>Daily PV Generation</label>
<category>Line</category>
<tags>
<tag>Measurement</tag>
<tag>Energy</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-total-pv-generation">
<item-type>Number:Energy</item-type>
<label>Total PV Generation</label>
<category>Line</category>
<tags>
<tag>Measurement</tag>
<tag>Energy</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-internal-temperature" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Internal Temperature</label>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-meter-connected" advanced="true">
<item-type>Contact</item-type>
<label>Meter Connected</label>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="fi-status-alarm" advanced="false">
<item-type>Contact</item-type>
<label>System Status: Alarm</label>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="fi-status-operation" advanced="false">
<item-type>Contact</item-type>
<label>System Status: Operation</label>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="fi-status-standby" advanced="true">
<item-type>Contact</item-type>
<label>System Status: Standby</label>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="fi-pv-power">
<item-type>Number:Power</item-type>
<label>PV Power</label>
<description>Current solar power generation from PV panels. Always positive.</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt1-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>MPPT1 Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt1-current" advanced="true">
<item-type>Number:ElectricCurrent</item-type>
<label>MPPT1 Current</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt1-power" advanced="false">
<item-type>Number:Power</item-type>
<label>MPPT1 Power</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt2-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>MPPT2 Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt2-power" advanced="false">
<item-type>Number:Power</item-type>
<label>MPPT2 Power</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt2-current" advanced="true">
<item-type>Number:ElectricCurrent</item-type>
<label>MPPT2 Current</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt3-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>MPPT3 Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt3-current" advanced="true">
<item-type>Number:ElectricCurrent</item-type>
<label>MPPT3 Current</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt3-power" advanced="false">
<item-type>Number:Power</item-type>
<label>MPPT3 Power</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt4-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>MPPT4 Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt4-power" advanced="false">
<item-type>Number:Power</item-type>
<label>MPPT4 Power</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-mppt4-current" advanced="true">
<item-type>Number:ElectricCurrent</item-type>
<label>MPPT4 Current</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-phase-a-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Phase A Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-phase-b-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Phase B Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-phase-c-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Phase C Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-grid-frequency" advanced="true">
<item-type>Number:Frequency</item-type>
<label>Grid Frequency</label>
<category>Line</category>
<tags>
<tag>Measurement</tag>
<tag>Frequency</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-grid-export-power">
<item-type>Number:Power</item-type>
<label>Grid Export Power</label>
<description>Power flow with public grid. Positive values indicate export to grid, negative values indicate import
from grid.</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-status-on-grid" advanced="true">
<item-type>Contact</item-type>
<label>On Grid</label>
<category>Energy</category>
<tags>
<tag>Status</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="fi-battery-voltage" advanced="true">
<item-type>Number:ElectricPotential</item-type>
<label>Battery Voltage</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Voltage</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-battery-current" advanced="true">
<item-type>Number:ElectricCurrent</item-type>
<label>Battery Current</label>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Current</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-battery-charging-power">
<item-type>Number:Power</item-type>
<label>Battery Charging Power</label>
<description>Battery power flow. Positive values indicate charging, negative values indicate discharging.</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-battery-level">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Battery Level</label>
<category>Battery</category>
<tags>
<tag>Measurement</tag>
<tag>Energy</tag>
</tags>
<state pattern="%.0f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-battery-temperature" advanced="false">
<item-type>Number:Temperature</item-type>
<label>Battery Temperature</label>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
<channel-type id="fi-battery-minimum-soc" advanced="true">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Battery minimum state of charge</label>
<category>Battery</category>
<tags>
<tag>Setpoint</tag>
<tag>Energy</tag>
</tags>
<state pattern="%.0f %unit%" readOnly="false"/>
</channel-type>
<channel-type id="fi-battery-maximum-soc" advanced="true">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Battery maximum state of charge</label>
<category>Battery</category>
<tags>
<tag>Setpoint</tag>
<tag>Energy</tag>
</tags>
<state pattern="%.0f %unit%" readOnly="false"/>
</channel-type>
<channel-type id="fi-battery-minimum-soc-on-grid" advanced="true">
<item-type unitHint="%">Number:Dimensionless</item-type>
<label>Battery minimum state of charge (on grid)</label>
<category>Battery</category>
<tags>
<tag>Setpoint</tag>
<tag>Energy</tag>
</tags>
<state pattern="%.0f %unit%" readOnly="false"/>
</channel-type>
<channel-type id="fi-eps-output">
<item-type>Switch</item-type>
<label>EPS Enabled</label>
<category>Energy</category>
<tags>
<tag>Control</tag>
<tag>Power</tag>
</tags>
<state readOnly="false"/>
</channel-type>
<channel-type id="fi-eps-export-power">
<item-type>Number:Power</item-type>
<label>EPS Export Power</label>
<description>Emergency power supply socket power. Positive values indicate supplying connected devices, negative
values indicate receiving power from external source.</description>
<category>Energy</category>
<tags>
<tag>Measurement</tag>
<tag>Power</tag>
</tags>
<state pattern="%.2f %unit%" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>
@@ -0,0 +1,73 @@
/*
* Copyright (c) 2010-2025 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.foxinverter.internal;
import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.core.io.transport.modbus.ModbusBitUtilities;
import org.openhab.core.io.transport.modbus.ModbusRegisterArray;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.types.State;
/**
* @author Sönke Küper - Initial contribution
* @author Holger Friedrich - Carry over from SunGrow, change BatteryLevel test to Number:Dimensionless
*/
@NonNullByDefault
class MQ2200InverterRegistersTest {
@Test
public void testCreatePercentTypeState() {
MQ2200InverterRegisters batteryLevelRegister = MQ2200InverterRegisters.BATTERY_LEVEL;
ModbusRegisterArray registers = new ModbusRegisterArray(100);
Optional<DecimalType> value = ModbusBitUtilities.extractStateFromRegisters( //
registers, //
0, //
batteryLevelRegister.getType() //
);
assertTrue(value.isPresent());
DecimalType decimalTypeValue = value.get();
// Value is not scaled yet
assertEquals(BigDecimal.valueOf(100), decimalTypeValue.toBigDecimal());
State state = batteryLevelRegister.createState(decimalTypeValue);
assertInstanceOf(QuantityType.class, state);
assertEquals("100 %", state.toFullString());
}
@Test
public void testCreateQuantityTypeState() {
MQ2200InverterRegisters mpptVoltage = MQ2200InverterRegisters.MPPT1_VOLTAGE;
ModbusRegisterArray registers = new ModbusRegisterArray(1234);
Optional<DecimalType> value = ModbusBitUtilities.extractStateFromRegisters( //
registers, //
0, //
mpptVoltage.getType() //
);
assertTrue(value.isPresent());
DecimalType decimalTypeValue = value.get();
// Value is not scaled yet
assertEquals(BigDecimal.valueOf(1234), decimalTypeValue.toBigDecimal());
State state = mpptVoltage.createState(decimalTypeValue);
assertInstanceOf(QuantityType.class, state);
assertEquals("123.4 V", state.toFullString());
}
}
+1
View File
@@ -276,6 +276,7 @@
<module>org.openhab.binding.minecraft</module>
<module>org.openhab.binding.modbus</module>
<module>org.openhab.binding.modbus.e3dc</module>
<module>org.openhab.binding.modbus.foxinverter</module>
<module>org.openhab.binding.modbus.kermi</module>
<module>org.openhab.binding.modbus.sbc</module>
<module>org.openhab.binding.modbus.studer</module>