From adccbff814238a2a6390766e49378b0bf9a14797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nagy=20Attila=20G=C3=A1bor?= Date: Sat, 18 Apr 2020 09:44:28 +0200 Subject: [PATCH] [modbus][sunspec] Initial contribution (#6331) * [sunspec] Modbus: SunSpec bundle basic version SunSpec is an open standard for solar inverters and other related devices to share data about their internal state. The standard is implemented by several vendors like ABB, Fronius, SMA, Schneider Electric, Solaredge, etc. The goal of this work is to add user friendly support of these devices to openHAB. The standard is built on the Modbus protocol, so this work is heavily based on the Modbus binding. Also the Bluetooth binding and several of it's solutions were taken as an example how to extend an already existing binding with a new bundle. Related issue: #3216 This is a stripped down version of a SunSpec bundle aimed to ease reviewing. Original PW with all the features can be found here: https://github.com/openhab/openhab2-addons/pull/4220 This version contains only a limited support for single phase inverters, but no auto discovery or any other fancy features are included. Other changes from my original PR: - migrated to the new build system - using NonNull values wherever possible - modell classes were moved to a dto package and highly simplified - other minor code changes Signed-off-by: Nagy Attila Gabor --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../.classpath | 32 ++ .../.project | 23 + .../org.openhab.binding.modbus.sunspec/NOTICE | 13 + .../README.md | 297 +++++++++++ .../pom.xml | 31 ++ .../src/main/feature/feature.xml | 10 + .../sunspec/internal/InverterStatus.java | 49 ++ .../internal/SunSpecConfiguration.java | 48 ++ .../sunspec/internal/SunSpecConstants.java | 85 +++ .../internal/SunSpecHandlerFactory.java | 86 +++ .../internal/dto/InverterModelBlock.java | 140 +++++ .../sunspec/internal/dto/ModelBlock.java | 43 ++ .../handler/AbstractSunSpecHandler.java | 495 ++++++++++++++++++ .../internal/handler/InverterHandler.java | 115 ++++ .../internal/parser/AbstractBaseParser.java | 126 +++++ .../internal/parser/InverterModelParser.java | 60 +++ .../internal/parser/SunspecParser.java | 42 ++ .../ESH-INF/config/config-descriptions.xml | 38 ++ .../ESH-INF/thing/inverter-channel-groups.xml | 30 ++ .../ESH-INF/thing/inverter-channel-types.xml | 85 +++ .../ESH-INF/thing/inverter-types.xml | 36 ++ bundles/pom.xml | 1 + 24 files changed, 1891 insertions(+) create mode 100644 bundles/org.openhab.binding.modbus.sunspec/.classpath create mode 100644 bundles/org.openhab.binding.modbus.sunspec/.project create mode 100644 bundles/org.openhab.binding.modbus.sunspec/NOTICE create mode 100644 bundles/org.openhab.binding.modbus.sunspec/README.md create mode 100644 bundles/org.openhab.binding.modbus.sunspec/pom.xml create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/InverterStatus.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConfiguration.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecConstants.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/SunSpecHandlerFactory.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/InverterModelBlock.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/dto/ModelBlock.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/AbstractSunSpecHandler.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/handler/InverterHandler.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/AbstractBaseParser.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/InverterModelParser.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/java/org/openhab/binding/modbus/sunspec/internal/parser/SunspecParser.java create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/config/config-descriptions.xml create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-groups.xml create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-channel-types.xml create mode 100644 bundles/org.openhab.binding.modbus.sunspec/src/main/resources/ESH-INF/thing/inverter-types.xml 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