diff --git a/CODEOWNERS b/CODEOWNERS
index cea7f118e50f4..95ee19be8ef2f 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -184,6 +184,7 @@
/bundles/org.openhab.binding.sonyprojector/ @lolodomo
/bundles/org.openhab.binding.spotify/ @Hilbrand
/bundles/org.openhab.binding.squeezebox/ @digitaldan @mhilbush
+/bundles/org.openhab.binding.modbus.sunspec/ @mrbig
/bundles/org.openhab.binding.synopanalyzer/ @clinique
/bundles/org.openhab.binding.systeminfo/ @svilenvul
/bundles/org.openhab.binding.tado/ @dfrommi
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index d4717076252d9..5c8ceba6fb7df 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -914,6 +914,11 @@
org.openhab.binding.squeezebox${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.modbus.sunspec
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.synopanalyzer
diff --git a/bundles/org.openhab.binding.modbus.sunspec/.classpath b/bundles/org.openhab.binding.modbus.sunspec/.classpath
new file mode 100644
index 0000000000000..a5d95095ccaaf
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/.project b/bundles/org.openhab.binding.modbus.sunspec/.project
new file mode 100644
index 0000000000000..8a0e055626fdb
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.binding.modbus.sunspec
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/NOTICE b/bundles/org.openhab.binding.modbus.sunspec/NOTICE
new file mode 100644
index 0000000000000..4c20ef446c1e4
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/NOTICE
@@ -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/openhab2-addons
diff --git a/bundles/org.openhab.binding.modbus.sunspec/README.md b/bundles/org.openhab.binding.modbus.sunspec/README.md
new file mode 100644
index 0000000000000..7ef7c37e2f3ef
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/README.md
@@ -0,0 +1,297 @@
+# Modbus: SunSpec Bundle
+
+This bundle is an extension for the Modbus binding to support the SunSpec protocol.
+
+SunSpec is a format for inverters and smart meters to communicate over the Modbus protocol.
+It defines how common parameters like AC/DC voltage and current, lifetime produced energy, device temperature etc can be read from the device.
+
+SunSpec is supported by several manufacturers like ABB, Fronius, LG, SMA, SolarEdge, Schneider Electric.
+For a list of certified products see this page: https://sunspec.org/sunspec-certified-products/
+
+# IMPORTANT: under merge
+
+** IMPORTANT: this version of this bundle is being merged into openHAB. This will be done in small steps - this means that not everything in this readme is supported at the moment **
+
+Currently supported features are:
+
+* basic values of single phase inverters without auto-discovery
+
+ For the complete version of this bundle please contact me!
+
+
+## Supported Things
+
+This bundle adds the following thing types to the Modbus binding.
+Note, that the things will show up under the Modbus binding.
+
+| Thing | Description |
+|-----------------------|-----------------------------------------------------------------------|
+| inverter-single-phase | For simple, single phase inverters |
+| inverter-split-phase | Split phase inverters (Japanese grid and 240V grid in North America) |
+| inverter-three-phase | Three phase inverters |
+| meter-single-phase | Single phase meters (AN or AB) |
+| meter-split-phase | Split single phase meters (ABN) |
+| meter-wye-phase | Wye connected three phase meters (ABCN) |
+| meter-delta-phase | Delta connected three phase meters (ABC) |
+
+
+
+## Binding Configuration
+
+This bundle requires the openHAB 2 compatible Modbus binding to be installed.
+Please refer to the Modbus binding configuration.
+This addon does not require any additional configuration.
+
+## Thing Configuration
+
+You need first to set up either a TCP or a Serial Modbus bridge according to the Modbus documentation.
+Things in this bundle will use the selected bridge to connect to the device.
+
+The preferred way to add new things is by using the discovery feature.
+This way the bundle will automatically detect if the Modbus bridge supports the SunSpec protocol and if so what type of models are available.
+It will automatically detect the register addresses for each model.
+
+### Auto discovering things
+
+This bingind fully supports modbus auto discovery, that means all supported profiles should appear in the inbox once you connect your device.
+
+Auto discovery is turned off by default in the modbus binding so you have to enable it manually.
+
+You can add `enableDiscovery=true` attribute to your bridge config, or you can enable it in the paper ui under the modbus tcp|serial slave thing.
+
+A typical bridge configuration would looke like this:
+
+```
+Bridge modbus:tcp:bridge [ host="10.0.0.2", port=502, id=1, enableDiscovery=true ]
+```
+
+### Adding things manually
+
+If you decide to add a thing manually then first you have to find out the start address of the model block and the length of it.
+While the length is usually fixed the address isn't.
+Please refer to your device's vendor documentation how model blocks are laid for your equipment.
+
+The following parameters are valid for all thing types:
+
+| Parameter | Type | Required | Default if ommitted | Description |
+|-----------|---------|----------|---------------------|-----------------------------------------|
+| address | integer | yes | N/A | Start address of the model block. |
+| length | integer | yes | N/A | Length of the model block. Setting this too short could cause problems during parsing |
+| refresh | integer | no | 5 | Poll inteval in seconds. Increase this if you encounter connection errors |
+| maxTries | integer | no | 3 | Number of retries when before giving up reading from this thing. |
+
+
+## Channels
+
+Channels are grouped into channel groups.
+Different things support a subset of the following groups.
+
+### Device information group (deviceInformation)
+
+This group contains general operational information about the device.
+
+| Channel ID | Item Type | Description |
+|-------------------------|-----------------------|------------------------------------------------------------------------------------|
+| cabinet-temperature | Number:Temperature | Temperature of the cabinet if supported in Celsius |
+| heatsink-temperature | Number:Temperature | Device heat sink temperature in Celsius |
+| transformer-temperature | Number:Temperature | Temperature of the transformer in Celsius |
+| other-temperature | Number:Temperature | Any other temperature reading not covered by the above items if available. Celsius |
+| status | String | Device status: OFF=Off, SLEEP=Sleeping/night mode, ON=On - producing power |
+
+Supported by: all inverter things
+
+### AC summary group (acGeneral)
+
+#### inverters
+
+This group contains summarized values for the AC side of the inverter.
+Even if the inverter supports multiple phases this group will appear only once.
+
+| Channel ID | Item Type | Description |
+|----------------------|------------------------|---------------------------------------------------------------------|
+| ac-total-current | Number:ElectricCurrent | Total AC current over all phases in Amperes |
+| ac-power | Number:Power | Actual AC power over all phases in Watts |
+| ac-frequency | Number:Frequency | Actual grid frequency |
+| ac-apparent-power | Number:Power | Actual AC apparent power |
+| ac-reactive-power | Number:Power | Actual AC reactive power |
+| ac-power-factor | Number:Dimensionless | Actual AC power factor (%) |
+| ac-lifetime-energy | Number:Energy | AC lifetime energy production for this device in WattHours |
+
+Supported by: all inverter things
+
+
+#### meters
+
+This group contains summarized values for the power meter over all phases.
+
+| Channel ID | Item Type | Description |
+|--------------------------------------|--------------------------|---------------------------------------------------------------------|
+| ac-total-current | Number:ElectricCurrent | Total AC current over all phases in Amperes |
+| ac-average-voltage-to-n | Number:ElectricPotential | Average Line to Neutral AC Voltage over all phases |
+| ac-average-voltage-to-next | Number:ElectricPotential | Average Line to Line AC Voltage over all phases |
+| ac-frequency | Number:Frequency | Actual grid frequency |
+| ac-total-real-power | Number:Power | Total Real Power over all phases(W) |
+| ac-total-apparent-power | Number:Power | Total Apparent Power over all phases (W) |
+| ac-total-reactive-power | Number:Power | Total Reactive Power over all phases (W) |
+| ac-average-power-factor | Number:Dimensionless | Average AC Power Factor over all phases (%) |
+| ac-total-exported-real-energy | Number:Energy | Total Real Energy Exported over all phases (Wh) |
+| ac-total-imported-real-energy | Number:Energy | Total Real Energy Imported over all phases (Wh) |
+| ac-total-exported-apparent-energy | Number:Energy | Total Apparent Energy Exported over all phases (VAh) |
+| ac-total-imported-apparent-energy | Number:Energy | Total Apparent Energy Imported over all phases (VAh) |
+| ac-total-imported-reactive-energy-q1 | Number:Energy | Total Reactive Energy Imported Quadrant 1 over all phases (VARh) |
+| ac-total-imported-reactive-energy-q2 | Number:Energy | Total Reactive Energy Imported Quadrant 2 over all phases (VARh) |
+| ac-total-exported-reactive-energy-q3 | Number:Energy | Total Reactive Energy Exported Quadrant 3 over all phases (VARh) |
+| ac-total-exported-reactive-energy-q4 | Number:Energy | Total Reactive Energy Exported Quadrant 4 over all phases (VARh) |
+
+Supported by: all meter things
+
+
+### AC phase specific group
+
+#### inverters
+
+This group describes values for a single phase of the inverter.
+There can be a maximum of three of this group named:
+
+acPhaseA: available for all inverter types
+
+acPhaseB: available for inverter-slit-phase and inverter-three-phase type inverters
+
+acPhaseC: available only for inverter-three-phase type inverters.
+
+| Channel ID | Item Type | Description |
+|----------------------|--------------------------|---------------------------------------------------------------------|
+| ac-phase-current | Number:ElectricCurrent | Actual current over this phase in Watts |
+| ac-voltage-to-next | Number:ElectricPotential | Voltage of this phase relative to the next phase, or to the ground in case of single phase inverter. Note: some single phase SolarEdge inverters incorrectly use this value to report the voltage to neutral value|
+| ac-voltage-to-n | Number:ElectricPotential | Voltage of this phase relative to the ground |
+
+Supported by: all inverter things
+
+#### meters
+
+This group holds values for a given line of the meter.
+There can be a maximum of three of this group named:
+
+acPhaseA: available for all meter types
+
+acPhaseB: available for meter-split-phase, meter-wye-phase and meter-delta-phase meters
+
+acPhaseC: available only for meter-wye-phase and meter-delta-phase meters type inverters.
+
+
+| Channel ID | Item Type | Description |
+|--------------------------------|--------------------------|---------------------------------------------------------------------|
+| ac-phase-current | Number:ElectricCurrent | Actual current over this line in Watts |
+| ac-voltage-to-n | Number:ElectricPotential | Voltage of this line relative to the neutral line |
+| ac-voltage-to-next | Number:ElectricPotential | Voltage of this line relative to the next line |
+| ac-real-power | Number:Power | AC Real Power value (W) |
+| ac-apparent-power | Number:Power | AC Apparent Power value |
+| ac-reactive-power | Number:Power | AC Reactive Power value |
+| ac-power-factor | Number:Dimensionless | AC Power Factor (%) |
+| ac-exported-real-energy | Number:Energy | Real Energy Exported (Wh |
+| ac-imported-real-energy | Number:Energy | Real Energy Imported (Wh) |
+| ac-exported-apparent-energy | Number:Energy | Apparent Energy Exported (VAh) |
+| ac-imported-apparent-energy | Number:Energy | Apparent Energy Imported (VAh) |
+| ac-imported-reactive-energy-q1 | Number:Energy | Reactive Energy Imported Quadrant 1 (VARh) |
+| ac-imported-reactive-energy-q2 | Number:Energy | Reactive Energy Imported Quadrant 2 (VARh) |
+| ac-exported-reactive-energy-q3 | Number:Energy | Reactive Energy Exported Quadrant 3 (VARh) |
+| ac-exported-reactive-energy-q4 | Number:Energy | Reactive Energy Exported Quadrant 4 (VARh) |
+
+
+Supported by: all meter things
+
+### DC general group
+
+This group contains summarized data for the DC side of the inverter.
+DC information is summarized even if the inverter has multiple strings.
+
+| Channel ID | Item Type | Description |
+|----------------------|--------------------------|---------------------------------------------------------------------|
+| dc-current | Number:ElectricCurrent | Actual DC current in Amperes |
+| dc-voltage | Number:ElectricPotential | Actual DC voltage |
+| dc-power | Number:Power | Actual DC power produced |
+
+Supported by: all inverter things
+
+
+## Full Example
+
+To configure a SunSpec inverter you have to set up a Modbus bridge with the connection parameters.
+The Modbus binding supports both TCP and Serial connections please choose the one that's appropriate for you.
+Please enable discovery on the bridge.
+
+Textual configuration is optional, you can set up everything using PaperUI.
+After adding the Modbus bridge and enabling discovery a scan will be initiated and if the device supports SunSpec then the known models will be added to the inbox with correct address configuration.
+
+### Thing Configuration
+
+The preferred way to add a SunSpec compatible Thing is through auto-discovery.
+Whoever if the auto-discovery would not work, advanced users could set up the thing through the config file.
+
+Please note that the nested bridge configuration does not work at the moment.
+Use the following flat format to set up the bridge and the inverter thing:
+
+```
+Bridge modbus:tcp:bridge [ host="hostname|ip", port=502, id=1, enableDiscovery=true ]
+Thing modbus:inverter-single-phase:bridge:se4000h "SE4000h" (modbus:tcp:modbusbridge) [ address=40069, length=52, refresh=15 ]
+```
+
+Note: make sure that refresh, port and id values are numerical, without quotes.
+
+### Item Configuration
+
+```
+Number Inverter_Temperature "Temperature [%.1f C]" {channel="modbus:inverter-single-phase:bridge:se4000h:deviceInformation#heatsink-temperature"}
+
+Number Inverter_AC_Power "AC Power [%d W]" {channel="modbus:inverter-single-phase:bridge:se4000h:acGeneral#ac-power"}
+
+Number Inverter_AC1_A "AC Current Phase 1 [%0.2f A]" {channel="modbus:inverter-single-phase:bridge:se4000h:acPhaseA#ac-phase-current"}
+
+```
+
+### Sitemap Configuration
+
+```
+ Text item=Inverter_Temperature
+ Text item=Inverter_AC_Current
+ Text item=Inverter_AC_Power
+ Chart item=Inverter_Temperature period=D refresh=600000
+ Chart item=Inverter_AC_Power period=D refresh=30000
+
+```
+
+## Vendor specific information
+
+### SolarEdge
+
+Newer models of SolarEdge inverters can be monitored over TCP, but you need to enable support in the inverter first.
+Refer to the "Modbus over TCP Configuration" chapter in this documentation: https://www.solaredge.com/sites/default/files/sunspec-implementation-technical-note.pdf
+
+Modbus connection is limited to a single client at a time, so make sure no other clients are using the port.
+
+## For Developers
+
+SunSpec is a big specification with many different type of devices.
+If you own or have access to an appliance that is not supported at the moment then your help is welcome.
+
+If you want to extend the bundle yourself, you have to do the followings:
+
+ - Define your thing type, channel types and channel groups according to openHAB development practices.
+ You can look at the meter and inverter types to get ideas how you can avoid repeating the same configuration over and over.
+
+ - Extend the `AbstractSunSpecHandler` and implement the handlePolledData method.
+ This method will be regularly called with the register data read from the appliance.
+ The method should parse the data and update the channels with them.
+
+ - The preferred way to parse the raw data is to write a parser for you model block type.
+ Your class should implement the `SunspecParser` class and it is preferred to extend the `AbstractBaseParser` class.
+ This base class has methods to accurately extract fields from the register array.
+
+ - The parser should only retrieve the data from the register array and return them in a block descriptor class.
+ Scaling and other higher level transformation should be done by the handler itself.
+
+ - To include your block type in auto discovery you have to add its id to the `SUPPORTED_THING_TYPES_UIDS` map in `SunSpecConstants`. This is enough for our discovery process to include your thing type in the results.
+
+
+If you have questions or need help don't hesitate to contact us over the OpenHAB community forums and github pages.
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/pom.xml b/bundles/org.openhab.binding.modbus.sunspec/pom.xml
new file mode 100644
index 0000000000000..46b734d1dc3d8
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/pom.xml
@@ -0,0 +1,31 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 2.5.4-SNAPSHOT
+
+
+ org.openhab.binding.modbus.sunspec
+
+ openHAB Add-ons :: Bundles :: SunSpec Bundle
+
+
+
+ org.openhab.addons.bundles
+ org.openhab.binding.modbus
+ ${project.version}
+ provided
+
+
+ org.openhab.addons.bundles
+ org.openhab.io.transport.modbus
+ ${project.version}
+ provided
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/feature/feature.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..765c7a0e6846d
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ file:${basedirRoot}/bundles/org.openhab.binding.modbus/target/feature/feature.xml
+
+
+ openhab-runtime-base
+ openhab-binding-modbus
+ mvn:org.openhab.addons.bundles/org.openhab.binding.modbus.sunspec/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/InverterStatus.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/InverterStatus.java
new file mode 100644
index 0000000000000..5e0a8af84efee
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/InverterStatus.java
@@ -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;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConfiguration.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConfiguration.java
new file mode 100644
index 0000000000000..1ceb55b877141
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConfiguration.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConstants.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConstants.java
new file mode 100644
index 0000000000000..a0aec53b16569
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConstants.java
@@ -0,0 +1,85 @@
+/**
+ * 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.Collections;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.openhab.binding.modbus.ModbusBindingConstants;
+
+/**
+ * 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");
+
+ // Block types
+ public static final int COMMON_BLOCK = 1;
+ public static final int INVERTER_SINGLE_PHASE = 101;
+
+ /**
+ * Map of the supported thing type uids, with their block type id
+ */
+ public static final Map SUPPORTED_THING_TYPES_UIDS = Collections
+ .singletonMap(INVERTER_SINGLE_PHASE, THING_TYPE_INVERTER_SINGLE_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";
+
+ // 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";
+
+ // 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;
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java
new file mode 100644
index 0000000000000..594dfdb02ef1c
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java
@@ -0,0 +1,86 @@
+/**
+ * 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.THING_TYPE_INVERTER_SINGLE_PHASE;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.openhab.binding.modbus.sunspec.internal.handler.InverterHandler;
+import org.openhab.io.transport.modbus.ModbusManager;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+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);
+
+ /**
+ * Reference to the modbus manager
+ */
+ private ModbusManager manager;
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Collections
+ .singleton(THING_TYPE_INVERTER_SINGLE_PHASE);
+
+ /**
+ * This factory needs a reference to the ModbusManager wich is provided
+ * by the org.openhab.io.transport.modbus bundle. Please make
+ * sure it's installed and enabled before using this bundle
+ *
+ * @param manager reference to the ModbusManager. We use this for modbus communication
+ */
+ @Activate
+ public SunSpecHandlerFactory(@Reference ModbusManager manager) {
+ this.manager = manager;
+ }
+
+ @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 (thingTypeUID.equals(THING_TYPE_INVERTER_SINGLE_PHASE)) {
+ logger.debug("New InverterHandler created");
+ return new InverterHandler(thing, manager);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/InverterModelBlock.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/InverterModelBlock.java
new file mode 100644
index 0000000000000..2b8ebe260c7ca
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/InverterModelBlock.java
@@ -0,0 +1,140 @@
+/**
+ * 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 Current scale factor
+ */
+ public Short acCurrentSF;
+
+ /**
+ * 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 acApparentPower;
+
+ /**
+ * Apparent power scale factor
+ */
+ public Optional acApparentPowerSF;
+
+ /**
+ * Reactive power
+ */
+ public Optional acReactivePower;
+
+ /**
+ * Reactive power scale factor
+ */
+ public Optional acReactivePowerSF;
+
+ /**
+ * Power factor
+ */
+ public Optional acPowerFactor;
+
+ /**
+ * Power factor scale factor
+ */
+ public Optional acPowerFactorSF;
+
+ /**
+ * AC Lifetime Energy production
+ */
+ public Long acEnergyLifetime;
+
+ /**
+ * AC Lifetime Energy scale factor
+ */
+ public Short acEnergyLifetimeSF;
+
+ /**
+ * Cabinet temperature
+ */
+ public Short temperatureCabinet;
+
+ /**
+ * Heat sink temperature
+ */
+ public Optional temperatureHeatsink;
+
+ /**
+ * Transformer temperature
+ */
+ public Optional temperatureTransformer;
+
+ /**
+ * Other temperature
+ */
+ public Optional temperatureOther;
+
+ /**
+ * Heat sink temperature scale factor
+ */
+ public Short temperatureSF;
+
+ /**
+ * Current operating state
+ */
+ public Integer status;
+
+ /**
+ * Vendor defined operating state or error code
+ */
+ public Optional statusVendor;
+
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/ModelBlock.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/ModelBlock.java
new file mode 100644
index 0000000000000..5c64296c7e23c
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/ModelBlock.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java
new file mode 100644
index 0000000000000..6c65059d8b629
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java
@@ -0,0 +1,495 @@
+/**
+ * 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.PROPERTY_UNIQUE_ADDRESS;
+
+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.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.thing.Bridge;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.ThingStatusInfo;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.State;
+import org.eclipse.smarthome.core.types.UnDefType;
+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.io.transport.modbus.BasicModbusReadRequestBlueprint;
+import org.openhab.io.transport.modbus.BasicPollTaskImpl;
+import org.openhab.io.transport.modbus.BitArray;
+import org.openhab.io.transport.modbus.ModbusManager;
+import org.openhab.io.transport.modbus.ModbusReadCallback;
+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.openhab.io.transport.modbus.endpoint.ModbusSlaveEndpoint;
+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;
+
+ /**
+ * This is the slave endpoint we're connecting to
+ */
+ protected volatile @Nullable ModbusSlaveEndpoint endpoint = null;
+
+ /**
+ * This is the slave id, we store this once initialization is complete
+ */
+ private volatile int slaveId;
+
+ /**
+ * Reference to the modbus manager
+ */
+ protected final ModbusManager managerRef;
+
+ /**
+ * 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, ModbusManager managerRef) {
+ super(thing);
+ this.managerRef = managerRef;
+ }
+
+ /**
+ * 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 (endpoint == null || config == null) {
+ logger.debug("Invalid endpoint/config/manager ref for sunspec handler");
+ return;
+ }
+
+ if (pollTask != null) {
+ return;
+ }
+
+ Optional mainBlock = getAddressFromConfig();
+ if (mainBlock.isPresent()) {
+ publishUniqueAddress(mainBlock.get());
+ updateStatus(ThingStatus.UNKNOWN);
+ registerPollTask(mainBlock.get());
+ } 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 configuration from main configuration
+ */
+ private Optional getAddressFromConfig() {
+ @Nullable
+ SunSpecConfiguration myconfig = config;
+ if (myconfig == null) {
+ return Optional.empty();
+ }
+ ModelBlock block = new ModelBlock();
+ block.address = myconfig.address;
+ block.length = myconfig.length;
+ return Optional.of(block);
+ }
+
+ /**
+ * Publish the unique address property if it has not been set before
+ */
+ private void publishUniqueAddress(ModelBlock block) {
+ Map 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 (endpoint != null) {
+ return;
+ }
+
+ ModbusEndpointThingHandler slaveEndpointThingHandler = getEndpointThingHandler();
+ if (slaveEndpointThingHandler == null) {
+ @SuppressWarnings("null")
+ String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("");
+ 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();
+
+ endpoint = slaveEndpointThingHandler.asSlaveEndpoint();
+ } catch (EndpointNotInitializedException e) {
+ // this will be handled below as endpoint remains null
+ }
+
+ if (endpoint == null) {
+ @SuppressWarnings("null")
+ String label = Optional.ofNullable(getBridge()).map(b -> b.getLabel()).orElse("");
+ 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() {
+ endpoint = 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
+ ModbusSlaveEndpoint myendpoint = endpoint;
+ @Nullable
+ SunSpecConfiguration myconfig = config;
+ if (myconfig == null || myendpoint == null) {
+ throw new IllegalStateException("registerPollTask called without proper configuration");
+ }
+
+ logger.debug("Setting up regular polling");
+
+ BasicModbusReadRequestBlueprint request = new BasicModbusReadRequestBlueprint(getSlaveId(),
+ ModbusReadFunctionCode.READ_MULTIPLE_REGISTERS, mainBlock.address, mainBlock.length, myconfig.maxTries);
+
+ pollTask = new BasicPollTaskImpl(myendpoint, request, new ModbusReadCallback() {
+
+ @Override
+ public void onRegisters(@Nullable ModbusReadRequestBlueprint request,
+ @Nullable ModbusRegisterArray registers) {
+ if (registers == null) {
+ logger.debug("Received empty register array on poll");
+ return;
+ }
+
+ handlePolledData(registers);
+
+ if (getThing().getStatus() != ThingStatus.ONLINE) {
+ updateStatus(ThingStatus.ONLINE);
+ }
+ }
+
+ @Override
+ public void onError(@Nullable ModbusReadRequestBlueprint request, @Nullable Exception error) {
+ handleError(error);
+ }
+
+ @Override
+ public void onBits(@Nullable ModbusReadRequestBlueprint request, @Nullable BitArray bits) {
+ // don't care, we don't expect this result
+ }
+ });
+
+ long refreshMillis = myconfig.getRefreshMillis();
+ if (pollTask != null) {
+ PollTask task = pollTask;
+ managerRef.registerRegularPoll(task, refreshMillis, 1000);
+ }
+ }
+
+ /**
+ * 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");
+ managerRef.unregisterRegularPoll(task);
+
+ pollTask = null;
+ }
+
+ /**
+ * Handle errors received during communication
+ */
+ protected void handleError(@Nullable Exception error) {
+ // Ignore all incoming data and errors if configuration is not correct
+ if (hasConfigurationError() || getThing().getStatus() == ThingStatus.OFFLINE) {
+ return;
+ }
+ String msg = "";
+ String cls = "";
+ if (error != null) {
+ cls = error.getClass().getName();
+ msg = error.getMessage();
+ }
+ 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 scaleFactor) {
+ if (!value.isPresent() || !scaleFactor.isPresent()) {
+ return UnDefType.UNDEF;
+ }
+ return getScaled(value.get().longValue(), scaleFactor.get());
+ }
+
+ /**
+ * 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) {
+ if (scaleFactor == 1) {
+ return new DecimalType(value.longValue());
+ }
+ return new DecimalType(BigDecimal.valueOf(value.longValue(), scaleFactor * -1));
+ }
+
+ /**
+ * 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 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(Number value, Short scaleFactor, Unit> unit) {
+ if (scaleFactor == 1) {
+ return new QuantityType<>(value.longValue(), unit);
+ }
+ return new QuantityType<>(BigDecimal.valueOf(value.longValue(), scaleFactor * -1), unit);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java
new file mode 100644
index 0000000000000..7950fe9d38062
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java
@@ -0,0 +1,115 @@
+/**
+ * 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.eclipse.smarthome.core.library.unit.SIUnits.CELSIUS;
+import static org.eclipse.smarthome.core.library.unit.SmartHomeUnits.*;
+import static org.openhab.binding.modbus.sunspec.internal.SunSpecConstants.*;
+
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.types.UnDefType;
+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.io.transport.modbus.ModbusManager;
+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, ModbusManager managerRef) {
+ super(thing, managerRef);
+ }
+
+ /**
+ * 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(new ChannelUID(getThing().getUID(), 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));
+
+ resetCommunicationError();
+ }
+
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/AbstractBaseParser.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/AbstractBaseParser.java
new file mode 100644
index 0000000000000..877aba47c0001
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/AbstractBaseParser.java
@@ -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.eclipse.smarthome.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 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 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 extractOptionalAcc32(ModbusRegisterArray raw, int index) {
+ return ModbusBitUtilities.extractStateFromRegisters(raw, index, ValueType.UINT32).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 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) 1);
+ }
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/InverterModelParser.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/InverterModelParser.java
new file mode 100644
index 0000000000000..eedfdbf159704
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/InverterModelParser.java
@@ -0,0 +1,60 @@
+/**
+ * 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 {
+
+ @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.acCurrentSF = extractSunSSF(raw, 6);
+ 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.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;
+ }
+
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/SunspecParser.java b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/SunspecParser.java
new file mode 100644
index 0000000000000..d678202ab86bb
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/SunspecParser.java
@@ -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 {
+
+ /**
+ * This method should parser an incoming register array and
+ * return a not-null sunspec block
+ */
+ T parse(ModbusRegisterArray raw);
+}
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/config/config-descriptions.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/config/config-descriptions.xml
new file mode 100644
index 0000000000000..8a3279e45db5f
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/config/config-descriptions.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+ Poll interval. Use zero to disable automatic polling.
+ 5
+
+
+
+
+ Start address of the model block
+ 40000
+ true
+
+
+
+
+ Length of the model block in 2 byte words
+ 61
+ true
+
+
+
+
+ 3
+ Number of tries when reading data, if some of the reading fail. For single try, enter 1.
+ true
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-groups.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-groups.xml
new file mode 100644
index 0000000000000..a6317563160c7
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-groups.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-types.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-types.xml
new file mode 100644
index 0000000000000..4c1ae82ea905b
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-types.xml
@@ -0,0 +1,85 @@
+
+
+
+
+ Number:ElectricCurrent
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Frequency
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Power
+
+
+
+
+
+ Number:Dimensionless
+
+
+
+
+
+ Number:Energy
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+ Number:Temperature
+
+
+
+
+
+
+ String
+
+ Device status
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-types.xml b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-types.xml
new file mode 100644
index 0000000000000..ae5455e8a2439
--- /dev/null
+++ b/bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-types.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+ Single phase inverter supporting SunSpec mapping over tcp modbus connection.
+ Inverter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ uniqueAddress
+
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index e51a17b9facb6..8a62132b7a79d 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -148,6 +148,7 @@
org.openhab.binding.milightorg.openhab.binding.minecraftorg.openhab.binding.modbus
+ org.openhab.binding.modbus.sunspecorg.openhab.binding.mqttorg.openhab.binding.mqtt.genericorg.openhab.binding.mqtt.homeassistant