diff --git a/CODEOWNERS b/CODEOWNERS index 716d162a66010..f46f42937acd7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -183,6 +183,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 b680367631b96..1798bdab2f494 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -909,6 +909,11 @@ org.openhab.binding.squeezebox ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.modbus.sunspec + ${project.version} + org.openhab.addons.bundles org.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 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 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 3268b931375bd..d14d88a038814 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -147,6 +147,7 @@ org.openhab.binding.milight org.openhab.binding.minecraft org.openhab.binding.modbus + org.openhab.binding.modbus.sunspec org.openhab.binding.mqtt org.openhab.binding.mqtt.generic org.openhab.binding.mqtt.homeassistant