From 11d98188a3bbd35bc51a855fe3d2e3b72d13275e Mon Sep 17 00:00:00 2001 From: Oleksandr Mishchuk Date: Mon, 29 Sep 2025 23:08:49 +0300 Subject: [PATCH 1/2] Fix creation of lookup channels to be of correct type Signed-off-by: Oleksandr Mishchuk --- .../binding/solarman/internal/typeprovider/ChannelUtils.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/typeprovider/ChannelUtils.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/typeprovider/ChannelUtils.java index a75a0bf9e2e34..ec811f387b014 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/typeprovider/ChannelUtils.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/typeprovider/ChannelUtils.java @@ -59,6 +59,10 @@ public static String getItemType(ParameterItem item) { uom = "UNKN"; } + if (item.hasLookup() || Boolean.TRUE.equals(item.getIsstr())) { + return CoreItemFactory.STRING; + } + return switch (rule) { case 5, 6, 7, 9 -> CoreItemFactory.STRING; case 8 -> CoreItemFactory.DATETIME; From 4234b0aa642933edddf38a7430b481b6bc9fb673 Mon Sep 17 00:00:00 2001 From: Oleksandr Mishchuk Date: Mon, 29 Sep 2025 23:09:44 +0300 Subject: [PATCH 2/2] Add ability to write Solarman registers, add new type of hybrid invertor Signed-off-by: Oleksandr Mishchuk --- .../org.openhab.binding.solarman/README.md | 227 ++-- .../internal/SolarmanLoggerHandler.java | 39 +- .../internal/channel/BaseChannelConfig.java | 1 + .../channel/SolarmanChannelManager.java | 2 + .../internal/defmodel/ParameterItem.java | 16 +- .../internal/enums/IntegerValueType.java | 24 + .../internal/modbus/SolarmanProtocol.java | 5 + .../internal/modbus/SolarmanRawProtocol.java | 124 +- .../internal/modbus/SolarmanV5Protocol.java | 118 +- .../updater/SolarmanChannelUpdater.java | 27 +- .../updater/SolarmanRegisterUpdater.java | 236 ++++ .../solarman/internal/util/ParserUtils.java | 40 + .../resources/OH-INF/thing/thing-types.xml | 1 + .../resources/definitions/deye_hybrid.yaml | 92 +- .../resources/definitions/deye_sg01hp3.yaml | 1043 +++++++++++++++++ .../resources/definitions/deye_sg04lp3.yaml | 308 +++++ .../modbus/SolarmanRawProtocolTest.java | 4 +- .../modbus/SolarmanV5ProtocolTest.java | 13 +- 18 files changed, 2127 insertions(+), 193 deletions(-) create mode 100644 bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/enums/IntegerValueType.java create mode 100644 bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanRegisterUpdater.java create mode 100644 bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/util/ParserUtils.java create mode 100644 bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg01hp3.yaml diff --git a/bundles/org.openhab.binding.solarman/README.md b/bundles/org.openhab.binding.solarman/README.md index 575a503b604e9..0e0efd47cfafd 100644 --- a/bundles/org.openhab.binding.solarman/README.md +++ b/bundles/org.openhab.binding.solarman/README.md @@ -39,24 +39,25 @@ The `inverterType` parameter governs what registers the binding will read from t Possible values: -| Inverter Type | Inverters supported | Notes | -|----------------------|---------------------------------------------------|------------------------------------------------------------------| -| deye_hybrid | DEYE/Sunsynk/SolArk Hybrid inverters | used when no lookup specified | -| deye_sg04lp3 | DEYE/Sunsynk/SolArk Hybrid 8/12K-SG04LP3 | e.g. 12K-SG04LP3-EU | -| deye_string | DEYE/Sunsynk/SolArk String inverters | e.g. SUN-4/5/6/7/8/10/12K-G03 Plus | -| deye_2mppt | DEYE Microinverter with 2 MPPT Trackers | e.g. SUN600G3-EU-230 / SUN800G3-EU-230 / SUN1000G3-EU-230 | -| deye_4mppt | DEYE Microinverter with 4 MPPT Trackers | e.g. SUN1300G3-EU-230 / SUN1600G3-EU-230 / SUN2000G3-EU-230 | -| sofar_lsw3 | SOFAR Inverters | | -| sofar_g3hyd | SOFAR Hybrid Three-Phase inverter | HYD 6000 or rebranded (three-phase), ex. ZCS Azzurro 3PH HYD-ZSS | -| sofar_hyd3k-6k-es | SOFAR Hybrid Single-Phase inverter | HYD 6000 or rebranded (single-phase), ex. ZCS Azzurro HYD-ZSS | -| solis_hybrid | SOLIS Hybrid inverter | | -| solid_1p8k-5g | SOLIS 1P8K-5G | | -| solis_3p-4g | SOLIS Three-Phase Inverter 4G Series | | -| solis_s6-gr1p | SOLIS Single-Phase Inverter S6-GR1P | | -| hyd-zss-hp-3k-6k | ZCS Azzurro Hybrid HP 3K-6K inverters | Rebranded Sofar models | -| kstar_hybrid | KSTAR Hybrid inverters | | -| sofar_wifikit | SOFAR WiFi Kit | | -| zcs_azzurro-ktl-v3 | ZCS Azzurro KTL-V3 inverters | ZCS Azzurro 3.3/4.4/5.5/6.6 KTL-V3 (rebranded Sofar KTLX-G3) | +| Inverter Type | Inverters supported | Notes | +|--------------------|----------------------------------------------------|------------------------------------------------------------------| +| deye_hybrid | DEYE/Sunsynk/SolArk Hybrid inverters | used when no lookup specified | +| deye_sg04lp3 | DEYE/Sunsynk/SolArk Hybrid 8/12K-SG04LP3 | e.g. 12K-SG04LP3-EU | +| deye_sg01hp3 | DEYE/Sunsynk/SolArk Hybrid HighVoltage 30K-SG01HP3 | e.g. 30K-SG01HP3-EU | +| deye_string | DEYE/Sunsynk/SolArk String inverters | e.g. SUN-4/5/6/7/8/10/12K-G03 Plus | +| deye_2mppt | DEYE Microinverter with 2 MPPT Trackers | e.g. SUN600G3-EU-230 / SUN800G3-EU-230 / SUN1000G3-EU-230 | +| deye_4mppt | DEYE Microinverter with 4 MPPT Trackers | e.g. SUN1300G3-EU-230 / SUN1600G3-EU-230 / SUN2000G3-EU-230 | +| sofar_lsw3 | SOFAR Inverters | | +| sofar_g3hyd | SOFAR Hybrid Three-Phase inverter | HYD 6000 or rebranded (three-phase), ex. ZCS Azzurro 3PH HYD-ZSS | +| sofar_hyd3k-6k-es | SOFAR Hybrid Single-Phase inverter | HYD 6000 or rebranded (single-phase), ex. ZCS Azzurro HYD-ZSS | +| solis_hybrid | SOLIS Hybrid inverter | | +| solid_1p8k-5g | SOLIS 1P8K-5G | | +| solis_3p-4g | SOLIS Three-Phase Inverter 4G Series | | +| solis_s6-gr1p | SOLIS Single-Phase Inverter S6-GR1P | | +| hyd-zss-hp-3k-6k | ZCS Azzurro Hybrid HP 3K-6K inverters | Rebranded Sofar models | +| kstar_hybrid | KSTAR Hybrid inverters | | +| sofar_wifikit | SOFAR WiFi Kit | | +| zcs_azzurro-ktl-v3 | ZCS Azzurro KTL-V3 inverters | ZCS Azzurro 3.3/4.4/5.5/6.6 KTL-V3 (rebranded Sofar KTLX-G3) | The `additionalRequests` allows the user to specify additional address ranges to be polled. The format of the value is `mb_functioncode1:start1-end1, mb_functioncode2:start2-end2,...` For example `"0x03:0x27D-0x27E"` will issue an additional read for Holding Registers between `0x27D` and `0x27E`. @@ -70,7 +71,7 @@ Thing solarman:logger:local [ hostname="x.x.x.x", inverterType="deye_sg04lp3", s } ``` -**Please note** As of this writing inverter types besides the `deye_sg04lp3` were not tested to work. +**Please note** As of this writing inverter types besides the `deye_sg04lp3` and `deye_sg01hp3` were not tested to work. If you have one of those inverters and it works, please drop me a message, if it doesn't work, please open an issue and I'll try to fix it. ## Channels @@ -79,69 +80,106 @@ The list of channels is not static, it is generated dynamically based on the inv This is the list you get for the `deye_sg04lp3` inverter type: -| Channel | Type | Read/Write | Description | -|------------------------------------------|--------|--------------|-------------------------------------------------------| -| alert-alert | Number | R | Alert \[0x0229,0x022A,0x022B,0x022C,0x022D,0x022E\] | -| battery-battery-current | Number | R | Battery Current \[0x024F\] | -| battery-battery-power | Number | R | Battery Power \[0x024E\] | -| battery-battery-soc | Number | R | Battery SOC \[0x024C\] | -| battery-battery-temperature | Number | R | Battery Temperature \[0x024A\] | -| battery-battery-voltage | Number | R | Battery Voltage \[0x024B\] | -| battery-daily-battery-charge | Number | R | Daily Battery Charge \[0x0202\] | -| battery-daily-battery-discharge | Number | R | Daily Battery Discharge \[0x0203\] | -| battery-total-battery-charge | Number | R | Total Battery Charge \[0x0204,0x0205\] | -| battery-total-battery-discharge | Number | R | Total Battery Discharge \[0x0206,0x0207\] | -| battery-battery-absorption-v | Number | R | Battery Absorption V \[0x0064\] | -| battery-battery-empty-v | Number | R | Battery Empty V \[0x0066\] | -| battery-battery-equalization-v | Number | R | Battery Equalization V \[0x0063\] | -| battery-battery-float-v | Number | R | Battery Float V \[0x0065\] | -| battery-battery-capacity | Number | R | Battery Capacity \[0x0066\] | -| battery-battery-max-a-charge | Number | R | Battery Max A Charge \[0x006C\] | -| battery-battery-max-a-discharge | Number | R | Battery Max A Discharge \[0x006D\] | -| grid-daily-energy-bought | Number | R | Daily Energy Bought \[0x0208\] | -| grid-daily-energy-sold | Number | R | Daily Energy Sold \[0x0209\] | -| grid-external-ct-l1-power | Number | R | External CT L1 Power \[0x0268\] | -| grid-external-ct-l2-power | Number | R | External CT L2 Power \[0x0269\] | -| grid-external-ct-l3-power | Number | R | External CT L3 Power \[0x026A\] | -| grid-grid-voltage-l1 | Number | R | Grid Voltage L1 \[0x0256\] | -| grid-grid-voltage-l2 | Number | R | Grid Voltage L2 \[0x0257\] | -| grid-grid-voltage-l3 | Number | R | Grid Voltage L3 \[0x0258\] | -| grid-internal-ct-l1-power | Number | R | Internal CT L1 Power \[0x025C\] | -| grid-internal-ct-l2-power | Number | R | Internal CT L2 Power \[0x025D\] | -| grid-internal-ct-l3-power | Number | R | Internal CT L3 Power \[0x025E\] | -| grid-total-energy-bought | Number | R | Total Energy Bought \[0x020A,0x020B\] | -| grid-total-energy-sold | Number | R | Total Energy Sold \[0x020C,0x020D\] | -| grid-total-grid-power | Number | R | Total Grid Power \[0x0271\] | -| grid-total-grid-production | Number | R | Total Grid Production \[0x020C,0x020D\] | -| inverter-ac-temperature | Number | R | AC Temperature \[0x021D\] | -| inverter-communication-board-version-no- | Number | R | Communication Board Version No \[0x0011\] | -| inverter-control-board-version-no- | Number | R | Control Board Version No \[0x000D\] | -| inverter-current-l1 | Number | R | Current L1 \[0x0276\] | -| inverter-current-l2 | Number | R | Current L2 \[0x0277\] | -| inverter-current-l3 | Number | R | Current L3 \[0x0278\] | -| inverter-dc-temperature | Number | R | DC Temperature \[0x021C\] | -| inverter-frequency | Number | R | Inverter Frequency \[0x27E\] | -| inverter-inverter-id | String | R | Inverter ID \[0x0003,0x0004,0x0005,0x0006,0x0007\] | -| inverter-inverter-l1-power | Number | R | Inverter L1 Power \[0x0279\] | -| inverter-inverter-l2-power | Number | R | Inverter L2 Power \[0x027A\] | -| inverter-inverter-l3-power | Number | R | Inverter L3 Power \[0x027B\] | -| solar-daily-production | Number | R | Daily Production \[0x0211\] | -| solar-pv1-current | Number | R | PV1 Current \[0x02A5\] | -| solar-pv1-power | Number | R | PV1 Power \[0x02A0\] | -| solar-pv1-voltage | Number | R | PV1 Voltage \[0x02A4\] | -| solar-pv2-current | Number | R | PV2 Current \[0x02A7\] | -| solar-pv2-power | Number | R | PV2 Power \[0x02A1\] | -| solar-pv2-voltage | Number | R | PV2 Voltage \[0x02A6\] | -| solar-total-production | Number | R | Total Production \[0x0216,0x0217\] | -| upload-daily-load-consumption | Number | R | Daily Load Consumption \[0x020E\] | -| upload-load-l1-power | Number | R | Load L1 Power \[0x028A\] | -| upload-load-l2-power | Number | R | Load L2 Power \[0x028B\] | -| upload-load-l3-power | Number | R | Load L3 Power \[0x028C\] | -| upload-load-voltage-l1 | Number | R | Load Voltage L1 \[0x0284\] | -| upload-load-voltage-l2 | Number | R | Load Voltage L2 \[0x0285\] | -| upload-load-voltage-l3 | Number | R | Load Voltage L3 \[0x0286\] | -| upload-total-load-consumption | Number | R | Total Load Consumption \[0x020F,0x0210\] | -| upload-total-load-power | Number | R | Total Load Power \[0x028D\] | +| Channel | Type | Read/Write | Description | +|------------------------------------------|--------|------------|------------------------------------------------------------------------------------------------------------------------------------------| +| alert-alert | Number | R | Alert \[0x0229,0x022A,0x022B,0x022C,0x022D,0x022E\] | +| battery-battery-current | Number | R | Battery Current \[0x024F\] | +| battery-battery-power | Number | R | Battery Power \[0x024E\] | +| battery-battery-soc | Number | R | Battery SOC \[0x024C\] | +| battery-battery-temperature | Number | R | Battery Temperature \[0x024A\] | +| battery-battery-voltage | Number | R | Battery Voltage \[0x024B\] | +| battery-daily-battery-charge | Number | R | Daily Battery Charge \[0x0202\] | +| battery-daily-battery-discharge | Number | R | Daily Battery Discharge \[0x0203\] | +| battery-total-battery-charge | Number | R | Total Battery Charge \[0x0204,0x0205\] | +| battery-total-battery-discharge | Number | R | Total Battery Discharge \[0x0206,0x0207\] | +| battery-battery-absorption-v | Number | R | Battery Absorption V \[0x0064\] | +| battery-battery-empty-v | Number | R | Battery Empty V \[0x0066\] | +| battery-battery-equalization-v | Number | R | Battery Equalization V \[0x0063\] | +| battery-battery-float-v | Number | R | Battery Float V \[0x0065\] | +| battery-battery-capacity | Number | R | Battery Capacity \[0x0066\] | +| battery-battery-max-a-charge | Number | R | Battery Max A Charge \[0x006C\] | +| battery-battery-max-a-discharge | Number | R | Battery Max A Discharge \[0x006D\] | +| grid-daily-energy-bought | Number | R | Daily Energy Bought \[0x0208\] | +| grid-daily-energy-sold | Number | R | Daily Energy Sold \[0x0209\] | +| grid-external-ct-l1-power | Number | R | External CT L1 Power \[0x0268\] | +| grid-external-ct-l2-power | Number | R | External CT L2 Power \[0x0269\] | +| grid-external-ct-l3-power | Number | R | External CT L3 Power \[0x026A\] | +| grid-grid-voltage-l1 | Number | R | Grid Voltage L1 \[0x0256\] | +| grid-grid-voltage-l2 | Number | R | Grid Voltage L2 \[0x0257\] | +| grid-grid-voltage-l3 | Number | R | Grid Voltage L3 \[0x0258\] | +| grid-internal-ct-l1-power | Number | R | Internal CT L1 Power \[0x025C\] | +| grid-internal-ct-l2-power | Number | R | Internal CT L2 Power \[0x025D\] | +| grid-internal-ct-l3-power | Number | R | Internal CT L3 Power \[0x025E\] | +| grid-total-energy-bought | Number | R | Total Energy Bought \[0x020A,0x020B\] | +| grid-total-energy-sold | Number | R | Total Energy Sold \[0x020C,0x020D\] | +| grid-total-grid-power | Number | R | Total Grid Power \[0x0271\] | +| grid-total-grid-production | Number | R | Total Grid Production \[0x020C,0x020D\] | +| inverter-ac-temperature | Number | R | AC Temperature \[0x021D\] | +| inverter-communication-board-version-no- | Number | R | Communication Board Version No \[0x0011\] | +| inverter-control-board-version-no- | Number | R | Control Board Version No \[0x000D\] | +| inverter-current-l1 | Number | R | Current L1 \[0x0276\] | +| inverter-current-l2 | Number | R | Current L2 \[0x0277\] | +| inverter-current-l3 | Number | R | Current L3 \[0x0278\] | +| inverter-dc-temperature | Number | R | DC Temperature \[0x021C\] | +| inverter-frequency | Number | R | Inverter Frequency \[0x27E\] | +| inverter-inverter-id | String | R | Inverter ID \[0x0003,0x0004,0x0005,0x0006,0x0007\] | +| inverter-inverter-l1-power | Number | R | Inverter L1 Power \[0x0279\] | +| inverter-inverter-l2-power | Number | R | Inverter L2 Power \[0x027A\] | +| inverter-inverter-l3-power | Number | R | Inverter L3 Power \[0x027B\] | +| solar-daily-production | Number | R | Daily Production \[0x0211\] | +| solar-pv1-current | Number | R | PV1 Current \[0x02A5\] | +| solar-pv1-power | Number | R | PV1 Power \[0x02A0\] | +| solar-pv1-voltage | Number | R | PV1 Voltage \[0x02A4\] | +| solar-pv2-current | Number | R | PV2 Current \[0x02A7\] | +| solar-pv2-power | Number | R | PV2 Power \[0x02A1\] | +| solar-pv2-voltage | Number | R | PV2 Voltage \[0x02A6\] | +| solar-total-production | Number | R | Total Production \[0x0216,0x0217\] | +| upload-daily-load-consumption | Number | R | Daily Load Consumption \[0x020E\] | +| upload-load-l1-power | Number | R | Load L1 Power \[0x028A\] | +| upload-load-l2-power | Number | R | Load L2 Power \[0x028B\] | +| upload-load-l3-power | Number | R | Load L3 Power \[0x028C\] | +| upload-load-voltage-l1 | Number | R | Load Voltage L1 \[0x0284\] | +| upload-load-voltage-l2 | Number | R | Load Voltage L2 \[0x0285\] | +| upload-load-voltage-l3 | Number | R | Load Voltage L3 \[0x0286\] | +| upload-total-load-consumption | Number | R | Total Load Consumption \[0x020F,0x0210\] | +| upload-total-load-power | Number | R | Total Load Power \[0x028D\] | +| tou-time-1 | String | R/W | Time Of Use Time Period 1 \[0x0094\] (time in format `HH:mm`) | +| tou-time-2 | String | R/W | Time Of Use Time Period 2 \[0x0095\] (time in format `HH:mm`) | +| tou-time-3 | String | R/W | Time Of Use Time Period 3 \[0x0096\] (time in format `HH:mm`) | +| tou-time-4 | String | R/W | Time Of Use Time Period 4 \[0x0097\] (time in format `HH:mm`) | +| tou-time-5 | String | R/W | Time Of Use Time Period 5 \[0x0098\] (time in format `HH:mm`) | +| tou-time-6 | String | R/W | Time Of Use Time Period 6 \[0x0099\] (time in format `HH:mm`) | +| tou-power-1 | Number | R/W | Time Of Use Power 1 \[0x009A\] | +| tou-power-2 | Number | R/W | Time Of Use Power 2 \[0x009B\] | +| tou-power-3 | Number | R/W | Time Of Use Power 3 \[0x009C\] | +| tou-power-4 | Number | R/W | Time Of Use Power 4 \[0x009D\] | +| tou-power-5 | Number | R/W | Time Of Use Power 5 \[0x009E\] | +| tou-power-6 | Number | R/W | Time Of Use Power 6 \[0x009F\] | +| tou-battery-soc-1 | Number | R/W | Time Of Use Battery SOC 1 \[0x00A6\] | +| tou-battery-soc-2 | Number | R/W | Time Of Use Battery SOC 2 \[0x00A7\] | +| tou-battery-soc-3 | Number | R/W | Time Of Use Battery SOC 3 \[0x00A8\] | +| tou-battery-soc-4 | Number | R/W | Time Of Use Battery SOC 4 \[0x00A9\] | +| tou-battery-soc-5 | Number | R/W | Time Of Use Battery SOC 5 \[0x00AA\] | +| tou-battery-soc-6 | Number | R/W | Time Of Use Battery SOC 6 \[0x00AB\] | +| tou-charge-enable-1 | String | R/W | Time Of Use Charge Enable 1 \[0x00AC\] (one of `Charge disabled`, `Grid charge`, `Generator charge` or `Grid charge + Generator charge`) | +| tou-charge-enable-2 | String | R/W | Time Of Use Charge Enable 2 \[0x00AD\] (one of `Charge disabled`, `Grid charge`, `Generator charge` or `Grid charge + Generator charge`) | +| tou-charge-enable-3 | String | R/W | Time Of Use Charge Enable 3 \[0x00AE\] (one of `Charge disabled`, `Grid charge`, `Generator charge` or `Grid charge + Generator charge`) | +| tou-charge-enable-4 | String | R/W | Time Of Use Charge Enable 4 \[0x00AF\] (one of `Charge disabled`, `Grid charge`, `Generator charge` or `Grid charge + Generator charge`) | +| tou-charge-enable-5 | String | R/W | Time Of Use Charge Enable 5 \[0x00B0\] (one of `Charge disabled`, `Grid charge`, `Generator charge` or `Grid charge + Generator charge`) | +| tou-charge-enable-6 | String | R/W | Time Of Use Charge Enable 6 \[0x00B1\] (one of `Charge disabled`, `Grid charge`, `Generator charge` or `Grid charge + Generator charge`) | + +## Writable Channels + +If you want to write into registers of the invertor you need to directly define your channels as writable, because by default channels are counted as read-only. For example, `deye_sg04lp3` and `deye_sg01hp3` have a full set of writable channels connected to `Time Of Use` functionality, but if you want to add custom writable channels you need to define it like this: + +```java +Thing solarman:logger:local [ hostname="x.x.x.x", inverterType="deye_sg04lp3", serialNumber="1234567890", additionalRequests="0x03:0x00A6-0x00A8" ] { + Channels: + Type number : tou-battery-soc-1 [scale="1", uom="%", rule="1", registers="0x00A6", readOnly="false"] +} +``` + +Note the **readOnly="false"** part here, which will make channel accept commands and write data to invertor register. ## Full Example @@ -231,6 +269,31 @@ Number:Power PV2_Power "PV2 Power [%d W]" Number:ElectricCurrent PV2_Current "PV2 Current [%.1f A]" (solarman) {channel="solarman:logger:local:solar-pv2-current", unit="A"} Number:ElectricPotential PV2_Voltage "PV2 Voltage [%d V]" (solarman) {channel="solarman:logger:local:solar-pv2-voltage", unit="V"} +String ToU_1_Time "ToU 1 Time [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-time-1", unit=""} +Number:Dimensionless ToU_1_Battery_SOC "ToU 1 Battery SOC [%d %%]" (solarman) {channel="solarman:logger:local:time-of-use-tou-battery-soc-1", unit="%"} +Number:Power ToU_1_Power "ToU 1 Power [%d W]" (solarman) {channel="solarman:logger:local:time-of-use-tou-power-1", unit="W"} +String ToU_1_Chare_Enable "ToU 1 Charge Enable [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-charge-enable-1", unit=""} +String ToU_2_Time "ToU 2 Time [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-time-2", unit=""} +Number:Dimensionless ToU_2_Battery_SOC "ToU 2 Battery SOC [%d %%]" (solarman) {channel="solarman:logger:local:time-of-use-tou-battery-soc-2", unit="%"} +Number:Power ToU_2_Power "ToU 2 Power [%d W]" (solarman) {channel="solarman:logger:local:time-of-use-tou-power-2", unit="W"} +String ToU_2_Chare_Enable "ToU 2 Charge Enable [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-charge-enable-2", unit=""} +String ToU_3_Time "ToU 3 Time [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-time-3", unit=""} +Number:Dimensionless ToU_3_Battery_SOC "ToU 3 Battery SOC [%d %%]" (solarman) {channel="solarman:logger:local:time-of-use-tou-battery-soc-3", unit="%"} +Number:Power ToU_3_Power "ToU 3 Power [%d W]" (solarman) {channel="solarman:logger:local:time-of-use-tou-power-3", unit="W"} +String ToU_3_Chare_Enable "ToU 3 Charge Enable [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-charge-enable-3", unit=""} +String ToU_4_Time "ToU 4 Time [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-time-4", unit=""} +Number:Dimensionless ToU_4_Battery_SOC "ToU 4 Battery SOC [%d %%]" (solarman) {channel="solarman:logger:local:time-of-use-tou-battery-soc-4", unit="%"} +Number:Power ToU_4_Power "ToU 4 Power [%d W]" (solarman) {channel="solarman:logger:local:time-of-use-tou-power-4", unit="W"} +String ToU_4_Chare_Enable "ToU 4 Charge Enable [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-charge-enable-4", unit=""} +String ToU_5_Time "ToU 5 Time [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-time-5", unit=""} +Number:Dimensionless ToU_5_Battery_SOC "ToU 5 Battery SOC [%d %%]" (solarman) {channel="solarman:logger:local:time-of-use-tou-battery-soc-5", unit="%"} +Number:Power ToU_5_Power "ToU 5 Power [%d W]" (solarman) {channel="solarman:logger:local:time-of-use-tou-power-5", unit="W"} +String ToU_5_Chare_Enable "ToU 5 Charge Enable [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-charge-enable-5", unit=""} +String ToU_6_Time "ToU 6 Time [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-time-6", unit=""} +Number:Dimensionless ToU_6_Battery_SOC "ToU 6 Battery SOC [%d %%]" (solarman) {channel="solarman:logger:local:time-of-use-tou-battery-soc-6", unit="%"} +Number:Power ToU_6_Power "ToU 6 Power [%d W]" (solarman) {channel="solarman:logger:local:time-of-use-tou-power-6", unit="W"} +String ToU_6_Chare_Enable "ToU 6 Charge Enable [%s]" (solarman) {channel="solarman:logger:local:time-of-use-tou-charge-enable-6", unit=""} + Number:Frequency Inverter_Frequency "Inverter Frequency [%.2f Hz]" (solarman) {channel="solarman:logger:local:inverter-frequency", unit="Hz"} ``` diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/SolarmanLoggerHandler.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/SolarmanLoggerHandler.java index e70604ffdceab..8664efcfa2ef8 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/SolarmanLoggerHandler.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/SolarmanLoggerHandler.java @@ -38,6 +38,8 @@ import org.openhab.binding.solarman.internal.modbus.SolarmanProtocolFactory; import org.openhab.binding.solarman.internal.updater.SolarmanChannelUpdater; import org.openhab.binding.solarman.internal.updater.SolarmanProcessResult; +import org.openhab.binding.solarman.internal.updater.SolarmanRegisterUpdater; +import org.openhab.binding.solarman.internal.util.ParserUtils; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; @@ -63,6 +65,8 @@ public class SolarmanLoggerHandler extends BaseThingHandler { private final SolarmanChannelManager solarmanChannelManager; @Nullable private volatile ScheduledFuture scheduledFuture; + @Nullable + private SolarmanRegisterUpdater solarmanRegisterUpdater; public SolarmanLoggerHandler(Thing thing) { super(thing); @@ -72,6 +76,13 @@ public SolarmanLoggerHandler(Thing thing) { @Override public void handleCommand(ChannelUID channelUID, Command command) { + logger.trace("Received command {} in channel {}", command, channelUID); + + if (solarmanRegisterUpdater != null) { + solarmanRegisterUpdater.updateLoggerRegisters(channelUID, command); + } else { + logger.error("SolarmanRegisterUpdater is not initialized yet"); + } } @Override @@ -110,6 +121,8 @@ public void initialize() { extractChannelMappingFromChannels(staticChannels), setupChannelsForInverterDefinition(inverterDefinition)); + solarmanRegisterUpdater = new SolarmanRegisterUpdater(paramToChannelMapping, solarmanLoggerConnector, + solarmanProtocol); SolarmanChannelUpdater solarmanChannelUpdater = new SolarmanChannelUpdater(this::updateState); scheduledFuture = scheduler @@ -152,19 +165,13 @@ private Map extractChannelMappingFromChannels(List(new ParameterItem(label, "N/A", "N/A", bcc.uom, bcc.scale, bcc.rule, - parseRegisters(bcc.registers), "N/A", new Validation(), bcc.offset, Boolean.FALSE, null), - channel.getUID()); + ParserUtils.parseRegisters(bcc.registers), "N/A", new Validation(), bcc.offset, Boolean.FALSE, + bcc.readOnly, null), channel.getUID()); }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - private List parseRegisters(String registers) { - String[] tokens = registers.split(","); - Pattern pattern = Pattern.compile("\\s*(0x[\\da-fA-F]+|[\\d]+)\\s*"); - return Stream.of(tokens).map(pattern::matcher).filter(Matcher::matches).map(matcher -> matcher.group(1)) - .map(SolarmanLoggerHandler::parseNumber).toList(); - } - // For now just concatenate the list, in the future, merge overlapping requests private List mergeRequests(List requestList1, List requestList2) { return Stream.concat(requestList1.stream(), requestList2.stream()).collect(Collectors.toList()); @@ -177,19 +184,15 @@ private List extractAdditionalRequests(String channels) { return Stream.of(tokens).map(pattern::matcher).filter(Matcher::matches).map(matcher -> { try { - int functionCode = parseNumber(matcher.group(1)); - int start = parseNumber(matcher.group(2)); - int end = parseNumber(matcher.group(3)); + int functionCode = ParserUtils.parseNumber(matcher.group(1)); + int start = ParserUtils.parseNumber(matcher.group(2)); + int end = ParserUtils.parseNumber(matcher.group(3)); return new Request(functionCode, start, end); } catch (NumberFormatException e) { logger.debug("Invalid number format in token: {} , ignoring additional requests", matcher.group(), e); - return new Request(-1, 0, 0); + return Request.NONE; } - }).filter(request -> request.getMbFunctioncode() > 0).collect(Collectors.toList()); - } - - private static int parseNumber(String number) { - return number.startsWith("0x") ? Integer.parseInt(number.substring(2), 16) : Integer.parseInt(number); + }).filter(request -> !Request.NONE.equals(request)).collect(Collectors.toList()); } private Map setupChannelsForInverterDefinition(InverterDefinition inverterDefinition) { diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/BaseChannelConfig.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/BaseChannelConfig.java index 4d76b8b1ac904..4e93b8bf2afe2 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/BaseChannelConfig.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/BaseChannelConfig.java @@ -27,4 +27,5 @@ public class BaseChannelConfig { public Integer rule = 1; public BigDecimal offset = BigDecimal.ZERO; public String registers = ""; + public boolean readOnly = true; } diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/SolarmanChannelManager.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/SolarmanChannelManager.java index 67078cf38ba62..9ab98dcc243c0 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/SolarmanChannelManager.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/channel/SolarmanChannelManager.java @@ -87,6 +87,8 @@ private Configuration buildConfigurationFromItem(ParameterItem item) { baseChannelConfig.rule = item.getRule(); } + baseChannelConfig.readOnly = !Boolean.FALSE.equals(item.getIsReadOnly()); + baseChannelConfig.registers = convertRegisters(item.getRegisters()); baseChannelConfig.uom = item.getUom(); diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/defmodel/ParameterItem.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/defmodel/ParameterItem.java index d50fbef551e21..e5652a8f3ef37 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/defmodel/ParameterItem.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/defmodel/ParameterItem.java @@ -46,6 +46,8 @@ public class ParameterItem { private BigDecimal offset; @Nullable private Boolean isstr; + @Nullable + private Boolean isReadOnly; private List lookup = new ArrayList<>(); public ParameterItem() { @@ -54,18 +56,20 @@ public ParameterItem() { public ParameterItem(String name, @Nullable String itemClass, @Nullable String stateClass, @Nullable String uom, @Nullable BigDecimal scale, Integer rule, List registers, @Nullable String icon, @Nullable Validation validation, @Nullable BigDecimal offset, @Nullable Boolean isstr, - @Nullable List lookup) { + @Nullable Boolean isReadOnly, @Nullable List lookup) { this.name = name; this.itemClass = itemClass; this.stateClass = stateClass; this.uom = uom; this.scale = scale; this.rule = rule; - this.registers = registers; + this.registers = new ArrayList<>(registers); + this.registers.sort(Integer::compareTo); this.icon = icon; this.validation = validation; this.offset = offset; this.isstr = isstr; + this.isReadOnly = isReadOnly; if (lookup != null) { this.lookup = lookup; } @@ -151,6 +155,14 @@ public void setIsstr(Boolean isstr) { this.isstr = isstr; } + public @Nullable Boolean getIsReadOnly() { + return isReadOnly; + } + + public void setIsReadOnly(Boolean isReadOnly) { + this.isReadOnly = isReadOnly; + } + public @Nullable String getItemClass() { return itemClass; } diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/enums/IntegerValueType.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/enums/IntegerValueType.java new file mode 100644 index 0000000000000..ffc1b55b0ab1b --- /dev/null +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/enums/IntegerValueType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarman.internal.enums; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Oleksandr Mishchuk - Initial contribution + */ +@NonNullByDefault +public enum IntegerValueType { + UNSIGNED, + SIGNED +} diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanProtocol.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanProtocol.java index fa26a4e4784bd..c3d94d547e457 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanProtocol.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanProtocol.java @@ -22,7 +22,12 @@ */ @NonNullByDefault public interface SolarmanProtocol { + byte DEFAULT_SLAVE_ID = 0x01; + byte WRITE_REGISTERS_FUNCTION_CODE = 0x10; Map readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode, int firstReg, int lastReg) throws SolarmanException; + + boolean writeRegisters(SolarmanLoggerConnection solarmanLoggerConnection, int register, byte[] data) + throws SolarmanException; } diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocol.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocol.java index 5cb385d445545..c4a4e8c70e662 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocol.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocol.java @@ -37,9 +37,11 @@ public SolarmanRawProtocol(SolarmanLoggerConfiguration solarmanLoggerConfigurati this.solarmanLoggerConfiguration = solarmanLoggerConfiguration; } + @Override public Map readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode, int firstReg, int lastReg) throws SolarmanException { - byte[] solarmanRawFrame = buildSolarmanRawFrame(mbFunctionCode, firstReg, lastReg); + int regCount = lastReg - firstReg + 1; + byte[] solarmanRawFrame = buildSolarmanRawReadFrame(mbFunctionCode, firstReg, regCount); byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanRawFrame); if (respFrame.length > 0) { byte[] modbusRespFrame = extractModbusRawResponseFrame(respFrame, solarmanRawFrame); @@ -49,6 +51,73 @@ public Map readRegisters(SolarmanLoggerConnection solarmanLogge } } + @Override + public boolean writeRegisters(SolarmanLoggerConnection solarmanLoggerConnection, int firstReg, byte[] data) + throws SolarmanException { + if (data.length % 2 != 0) { + throw new SolarmanException("Data to be written should be packed as two bytes per register!"); + } + + byte[] solarmanRawFrame = buildSolarmanRawWriteFrame(firstReg, data); + byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanRawFrame); + if (respFrame.length > 0) { + byte[] modbusRespFrame = extractModbusRawResponseFrame(respFrame, solarmanRawFrame); + parseRawModbusWriteHoldingRegistersResponse(modbusRespFrame, data); + return true; + } else { + throw new SolarmanConnectionException("Response frame was empty"); + } + } + + /** + * Builds a SolarMAN Raw frame to request data write from firstReg. + * Frame format is based on + * Solarman RAW Protocol + * + * @param firstReg - the start register + * @param data - the data to be written + * @return byte array containing the Solarman Raw frame + */ + private byte[] buildSolarmanRawWriteFrame(int firstReg, byte[] data) throws SolarmanException { + byte[] requestPayload = buildSolarmanRawWriteFrameRequestPayload(DEFAULT_SLAVE_ID, firstReg, data); + byte[] header = buildSolarmanRawFrameHeader(requestPayload.length); + + return ByteBuffer.allocate(header.length + requestPayload.length).put(header).put(requestPayload).array(); + } + + /** + * Builds a SolarMAN Raw write frame payload + * Frame format is based on + * Modbus RTU Write Multiple Registers + * + * @param slaveId - Modbus slave ID + * @param firstReg - the start register + * @param data - the data to be written + * @return byte array containing the Modbus RTU Raw frame payload + */ + private byte[] buildSolarmanRawWriteFrameRequestPayload(byte slaveId, int firstReg, byte[] data) + throws SolarmanException { + if (data.length % 2 != 0) { + throw new SolarmanException("Data to be written should be packed as two bytes per register!"); + } + + // slaveId (1 byte) + // mbFunction (1 byte) + // firstRegister (2 bytes) + // registerCount (2 bytes) + // data length (1 byte) + // data + int bufferSize = 1 + 1 + 2 + 2 + 1 + data.length; + int registerCount = data.length / 2; + + byte[] req = ByteBuffer.allocate(bufferSize).put(slaveId).put(WRITE_REGISTERS_FUNCTION_CODE) + .putShort((short) firstReg).putShort((short) registerCount).put((byte) data.length).put(data).array(); + byte[] crc = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN) + .putShort((short) CRC16Modbus.calculate(req)).array(); + + return ByteBuffer.allocate(req.length + crc.length).put(req).put(crc).array(); + } + protected byte[] extractModbusRawResponseFrame(byte @Nullable [] responseFrame, byte[] requestFrame) throws SolarmanException { if (responseFrame == null || responseFrame.length == 0) { @@ -81,6 +150,36 @@ protected Map parseRawModbusReadHoldingRegistersResponse(byte @ return registers; } + private void parseRawModbusWriteHoldingRegistersResponse(byte[] frame, byte[] data) + throws SolarmanProtocolException { + int expectedRegistersCount = data.length / 2; + + // slaveId (1 byte) + // modbusFunction (1 byte) + // firstRegister (2 bytes) + // registerCount (2 bytes) + int expectedFrameDataLen = 1 + 1 + 2 + 2; + if (frame == null || frame.length < expectedFrameDataLen + 2) { + throw new SolarmanProtocolException("Modbus frame is too short or empty"); + } + + int actualCrc = ByteBuffer.wrap(frame, expectedFrameDataLen, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() + & 0xFFFF; + int expectedCrc = CRC16Modbus.calculate(Arrays.copyOfRange(frame, 0, expectedFrameDataLen)); + + if (actualCrc != expectedCrc) { + throw new SolarmanProtocolException( + String.format("Modbus frame crc is not valid. Expected %04x, got %04x", expectedCrc, actualCrc)); + } + + short registersWrittenCount = ByteBuffer.wrap(frame, 4, 2).getShort(); + if (registersWrittenCount != expectedRegistersCount) { + throw new SolarmanProtocolException( + String.format("Modbus written registers count is not valid. Expected %04x, got %04x", + expectedRegistersCount, registersWrittenCount)); + } + } + /** * Builds a SolarMAN Raw frame to request data from firstReg to lastReg. * Frame format is based on @@ -94,14 +193,14 @@ protected Map parseRawModbusReadHoldingRegistersResponse(byte @ * Payload 0003: 1st register address * Payload 006e: Nb of registers to read * Trailer 3426: CRC-16 ModBus - * - * @param mbFunctionCode + * + * @param mbFunctionCode - Modbus function code * @param firstReg - the start register - * @param lastReg - the end register + * @param regCount - the registers count * @return byte array containing the Solarman Raw frame */ - protected byte[] buildSolarmanRawFrame(byte mbFunctionCode, int firstReg, int lastReg) { - byte[] requestPayload = buildSolarmanRawFrameRequestPayload(mbFunctionCode, firstReg, lastReg); + protected byte[] buildSolarmanRawReadFrame(byte mbFunctionCode, int firstReg, int regCount) { + byte[] requestPayload = buildSolarmanRawReadFrameRequestPayload(mbFunctionCode, firstReg, regCount); byte[] header = buildSolarmanRawFrameHeader(requestPayload.length); return ByteBuffer.allocate(header.length + requestPayload.length).put(header).put(requestPayload).array(); @@ -115,7 +214,7 @@ protected byte[] buildSolarmanRawFrame(byte mbFunctionCode, int firstReg, int la * Header 03e8: Transaction identifier * Header 0000: Protocol identifier * Header 0006: Message length (w/o CRC) - * + * * @param payloadSize th * @return byte array containing the Solarman Raw frame header */ @@ -136,7 +235,7 @@ private byte[] buildSolarmanRawFrameHeader(int payloadSize) { } /** - * Builds a SolarMAN Raw frame payload + * Builds a SolarMAN Raw read frame payload * Frame format is based on * Solarman RAW Protocol * Request send: @@ -145,14 +244,13 @@ private byte[] buildSolarmanRawFrameHeader(int payloadSize) { * Payload 0003: 1st register address * Payload 006e: Nb of registers to read * Trailer 3426: CRC-16 ModBus - * - * @param mbFunctionCode + * + * @param mbFunctionCode - Modbus function code * @param firstReg - the start register - * @param lastReg - the end register + * @param regCount - the registers count * @return byte array containing the Solarman Raw frame payload */ - protected byte[] buildSolarmanRawFrameRequestPayload(byte mbFunctionCode, int firstReg, int lastReg) { - int regCount = lastReg - firstReg + 1; + protected byte[] buildSolarmanRawReadFrameRequestPayload(byte mbFunctionCode, int firstReg, int regCount) { byte[] req = ByteBuffer.allocate(6).put((byte) 0x01).put(mbFunctionCode).putShort((short) firstReg) .putShort((short) regCount).array(); byte[] crc = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN) diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5Protocol.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5Protocol.java index c763e7b54f583..9da515a9fcda6 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5Protocol.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5Protocol.java @@ -40,7 +40,9 @@ public SolarmanV5Protocol(SolarmanLoggerConfiguration solarmanLoggerConfiguratio @Override public Map readRegisters(SolarmanLoggerConnection solarmanLoggerConnection, byte mbFunctionCode, int firstReg, int lastReg) throws SolarmanException { - byte[] solarmanV5Frame = buildSolarmanV5Frame(mbFunctionCode, firstReg, lastReg); + int regCount = lastReg - firstReg + 1; + byte[] modbusFrame = buildModbusReadHoldingRegistersFrame(DEFAULT_SLAVE_ID, mbFunctionCode, firstReg, regCount); + byte[] solarmanV5Frame = buildSolarmanV5Frame(modbusFrame); byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanV5Frame); if (respFrame.length > 0) { byte[] modbusRespFrame = extractModbusResponseFrame(respFrame, solarmanV5Frame); @@ -50,18 +52,31 @@ public Map readRegisters(SolarmanLoggerConnection solarmanLogge } } + @Override + public boolean writeRegisters(SolarmanLoggerConnection solarmanLoggerConnection, int firstReg, byte[] data) + throws SolarmanException { + byte[] modbusFrame = buildModbusWriteHoldingRegistersFrame(DEFAULT_SLAVE_ID, firstReg, data); + byte[] solarmanV5Frame = buildSolarmanV5Frame(modbusFrame); + byte[] respFrame = solarmanLoggerConnection.sendRequest(solarmanV5Frame); + if (respFrame.length > 0) { + byte[] modbusRespFrame = extractModbusResponseFrame(respFrame, solarmanV5Frame); + parseModbusWriteHoldingRegistersResponse(modbusRespFrame, data); + return true; + } else { + throw new SolarmanConnectionException("Response frame was empty"); + } + } + /** - * Builds a SolarMAN V5 frame to request data from firstReg to lastReg. + * Builds a SolarMAN V5 frame. * Frame format is based on * Solarman V5 Protocol * - * @param mbFunctionCode - * @param firstReg - the start register - * @param lastReg - the end register + * @param modbusPayload - ModBus Frame to integrate * @return byte array containing the Solarman V5 frame */ - protected byte[] buildSolarmanV5Frame(byte mbFunctionCode, int firstReg, int lastReg) { - byte[] requestPayload = buildSolarmanV5FrameRequestPayload(mbFunctionCode, firstReg, lastReg); + protected byte[] buildSolarmanV5Frame(byte[] modbusPayload) { + byte[] requestPayload = buildSolarmanV5FrameRequestPayload(modbusPayload); byte[] header = buildSolarmanV5FrameHeader(requestPayload.length); byte[] trailer = buildSolarmanV5FrameTrailer(header, requestPayload); @@ -125,7 +140,7 @@ private byte[] buildSolarmanV5FrameHeader(int payloadSize) { .put(start).put(length).put(controlCode).put(serial).put(loggerSerial).array(); } - protected byte[] buildSolarmanV5FrameRequestPayload(byte mbFunctionCode, int firstReg, int lastReg) { + protected byte[] buildSolarmanV5FrameRequestPayload(byte[] modbusPayload) { // (one byte) – Denotes the frame type. byte[] frameType = new byte[] { 0x02 }; // (two bytes) – Denotes the sensor type. @@ -137,15 +152,12 @@ protected byte[] buildSolarmanV5FrameRequestPayload(byte mbFunctionCode, int fir byte[] powerOnTime = new byte[] { 0x00, 0x00, 0x00, 0x00 }; // Denotes the frame offset time. byte[] offsetTime = new byte[] { 0x00, 0x00, 0x00, 0x00 }; - // (variable length) – Modbus RTU request frame. - byte[] requestFrame = buildModbusReadHoldingRegistersRequestFrame((byte) 0x01, mbFunctionCode, firstReg, - lastReg); return ByteBuffer .allocate(frameType.length + sensorType.length + totalWorkingTime.length + powerOnTime.length - + offsetTime.length + requestFrame.length) - .put(frameType).put(sensorType).put(totalWorkingTime).put(powerOnTime).put(offsetTime).put(requestFrame) - .array(); + + offsetTime.length + modbusPayload.length) + .put(frameType).put(sensorType).put(totalWorkingTime).put(powerOnTime).put(offsetTime) + .put(modbusPayload).array(); } /** @@ -153,16 +165,48 @@ protected byte[] buildSolarmanV5FrameRequestPayload(byte mbFunctionCode, int fir * Registers * * @param slaveId - Slave Address - * @param mbFunctionCode - - * @param firstReg - Starting Address - * @param lastReg - Ending Address + * @param mbFunctionCode - Modbus function code + * @param startRegister - Starting register + * @param registerCount - Register count * @return byte array containing the Modbus request frame */ - protected byte[] buildModbusReadHoldingRegistersRequestFrame(byte slaveId, byte mbFunctionCode, int firstReg, - int lastReg) { - int regCount = lastReg - firstReg + 1; - byte[] req = ByteBuffer.allocate(6).put(slaveId).put(mbFunctionCode).putShort((short) firstReg) - .putShort((short) regCount).array(); + protected byte[] buildModbusReadHoldingRegistersFrame(byte slaveId, byte mbFunctionCode, int startRegister, + int registerCount) { + byte[] req = ByteBuffer.allocate(6).put(slaveId).put(mbFunctionCode).putShort((short) startRegister) + .putShort((short) registerCount).array(); + byte[] crc = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN) + .putShort((short) CRC16Modbus.calculate(req)).array(); + + return ByteBuffer.allocate(req.length + crc.length).put(req).put(crc).array(); + } + + /** + * Based on Function 16 (10hex) Write Holding + * Registers + * + * @param slaveId - Slave Address + * @param startRegister - Starting register + * @param data - Data to be written + * @return byte array containing the Modbus request frame + */ + protected byte[] buildModbusWriteHoldingRegistersFrame(byte slaveId, int startRegister, byte[] data) + throws SolarmanException { + if (data.length % 2 != 0) { + throw new SolarmanException("Data to be written should be packed as two bytes per register!"); + } + + // slaveId (1 byte) + // mbFunction (1 byte) + // startRegister (2 bytes) + // registerCount (2 bytes) + // data length (1 byte) + // data + int bufferSize = 1 + 1 + 2 + 2 + 1 + data.length; + int registerCount = data.length / 2; + + byte[] req = ByteBuffer.allocate(bufferSize).put(slaveId).put(WRITE_REGISTERS_FUNCTION_CODE) + .putShort((short) startRegister).putShort((short) registerCount).put((byte) data.length).put(data) + .array(); byte[] crc = ByteBuffer.allocate(Short.BYTES).order(ByteOrder.LITTLE_ENDIAN) .putShort((short) CRC16Modbus.calculate(req)).array(); @@ -197,6 +241,36 @@ protected Map parseModbusReadHoldingRegistersResponse(byte @Nul return registers; } + protected void parseModbusWriteHoldingRegistersResponse(byte @Nullable [] frame, byte[] data) + throws SolarmanProtocolException { + int expectedRegistersCount = data.length / 2; + + // slaveId (1 byte) + // modbusFunction (1 byte) + // firstRegister (2 bytes) + // registerCount (2 bytes) + int expectedFrameDataLen = 1 + 1 + 2 + 2; + if (frame == null || frame.length < expectedFrameDataLen + 2) { + throw new SolarmanProtocolException("Modbus frame is too short or empty"); + } + + int actualCrc = ByteBuffer.wrap(frame, expectedFrameDataLen, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() + & 0xFFFF; + int expectedCrc = CRC16Modbus.calculate(Arrays.copyOfRange(frame, 0, expectedFrameDataLen)); + + if (actualCrc != expectedCrc) { + throw new SolarmanProtocolException( + String.format("Modbus frame crc is not valid. Expected %04x, got %04x", expectedCrc, actualCrc)); + } + + short registersWrittenCount = ByteBuffer.wrap(frame, 4, 2).getShort(); + if (registersWrittenCount != expectedRegistersCount) { + throw new SolarmanProtocolException( + String.format("Modbus written registers count is not valid. Expected %04x, got %04x", + expectedRegistersCount, registersWrittenCount)); + } + } + protected byte[] extractModbusResponseFrame(byte @Nullable [] responseFrame, byte[] requestFrame) throws SolarmanException { if (responseFrame == null || responseFrame.length == 0) { diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanChannelUpdater.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanChannelUpdater.java index e7cccaf14aa6f..988071a025be8 100644 --- a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanChannelUpdater.java +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanChannelUpdater.java @@ -36,6 +36,7 @@ import org.openhab.binding.solarman.internal.defmodel.Lookup; import org.openhab.binding.solarman.internal.defmodel.ParameterItem; import org.openhab.binding.solarman.internal.defmodel.Request; +import org.openhab.binding.solarman.internal.enums.IntegerValueType; import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnection; import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector; import org.openhab.binding.solarman.internal.modbus.SolarmanProtocol; @@ -100,9 +101,9 @@ private void updateChannelsForReadRegisters(Map param if (readRegistersMap.keySet().containsAll(registers)) { switch (parameterItem.getRule()) { case 1, 3 -> updateChannelWithNumericValue(parameterItem, channelUID, registers, readRegistersMap, - ValueType.UNSIGNED); + IntegerValueType.UNSIGNED); case 2, 4 -> updateChannelWithNumericValue(parameterItem, channelUID, registers, readRegistersMap, - ValueType.SIGNED); + IntegerValueType.SIGNED); case 5 -> updateChannelWithStringValue(channelUID, registers, readRegistersMap); case 6 -> updateChannelWithRawValue(parameterItem, channelUID, registers, readRegistersMap); case 7 -> updateChannelWithVersion(channelUID, registers, readRegistersMap); @@ -175,7 +176,7 @@ private void updateChannelWithStringValue(ChannelUID channelUID, List r } private void updateChannelWithNumericValue(ParameterItem parameterItem, ChannelUID channelUID, - List registers, Map readRegistersMap, ValueType valueType) { + List registers, Map readRegistersMap, IntegerValueType valueType) { BigInteger value = extractNumericValue(registers, readRegistersMap, valueType); BigDecimal convertedValue = convertNumericValue(value, parameterItem.getOffset(), parameterItem.getScale()); String uom = Objects.requireNonNullElse(parameterItem.getUom(), ""); @@ -229,19 +230,13 @@ private BigDecimal convertNumericValue(BigInteger value, @Nullable BigDecimal of } private BigInteger extractNumericValue(List registers, Map readRegistersMap, - ValueType valueType) { - return reversed(registers) - .stream().map(readRegistersMap::get).reduce( - BigInteger.ZERO, (acc, - val) -> acc.shiftLeft(Short.SIZE) - .add(BigInteger.valueOf(ByteBuffer.wrap(val).getShort() - & (valueType == ValueType.UNSIGNED ? 0xFFFF : 0xFFFFFFFF))), - BigInteger::add); - } - - private enum ValueType { - UNSIGNED, - SIGNED + IntegerValueType valueType) { + return reversed(registers).stream().map(readRegistersMap::get).reduce( + BigInteger.ZERO, (acc, + val) -> acc.shiftLeft(Short.SIZE) + .add(BigInteger.valueOf(ByteBuffer.wrap(val).getShort() + & (valueType == IntegerValueType.UNSIGNED ? 0xFFFF : 0xFFFFFFFF))), + BigInteger::add); } @FunctionalInterface diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanRegisterUpdater.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanRegisterUpdater.java new file mode 100644 index 0000000000000..9b4b99951dda8 --- /dev/null +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/updater/SolarmanRegisterUpdater.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarman.internal.updater; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.solarman.internal.defmodel.Lookup; +import org.openhab.binding.solarman.internal.defmodel.ParameterItem; +import org.openhab.binding.solarman.internal.enums.IntegerValueType; +import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnection; +import org.openhab.binding.solarman.internal.modbus.SolarmanLoggerConnector; +import org.openhab.binding.solarman.internal.modbus.SolarmanProtocol; +import org.openhab.binding.solarman.internal.modbus.exception.SolarmanConnectionException; +import org.openhab.binding.solarman.internal.modbus.exception.SolarmanException; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.util.HexUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link SolarmanRegisterUpdater} is responsible for updating registers from received commands + * + * @author Oleksandr Mishchuk - Initial contribution + */ +@NonNullByDefault +public class SolarmanRegisterUpdater { + private static final Logger logger = LoggerFactory.getLogger(SolarmanRegisterUpdater.class); + + private static final Pattern TIME_PATTERN = Pattern.compile("^([0-1][0-9])|(2[0-3]):[0-5][0-9]"); + + private final SolarmanLoggerConnector solarmanLoggerConnector; + private final SolarmanProtocol solarmanProtocol; + private final Map writableChannels; + + public SolarmanRegisterUpdater(Map paramToChannelMapping, + SolarmanLoggerConnector solarmanLoggerConnector, SolarmanProtocol solarmanProtocol) { + this.solarmanProtocol = solarmanProtocol; + this.solarmanLoggerConnector = solarmanLoggerConnector; + + writableChannels = paramToChannelMapping.entrySet().stream() + .filter(e -> Boolean.FALSE.equals(e.getKey().getIsReadOnly())).filter(e -> { + final List registers = e.getKey().getRegisters(); + if (registers.isEmpty()) { + logger.warn("Writeable channel {} have no registers, skipping", e.getValue()); + return false; + } + + final int firstRegister = registers.getFirst(); + int i = 0; + while (i < registers.size() && registers.get(i) == firstRegister + i) { + i++; + } + if (i != registers.size()) { + logger.warn("Writeable channel {} should have consecutive registers, skipping", e.getValue()); + return false; + } + + return true; + }).collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); + } + + public void updateLoggerRegisters(ChannelUID channelUID, Command command) { + // Ignore refresh command as we are using polling for reading data + if (command instanceof RefreshType) { + return; + } + + final ParameterItem channelToUpdate = writableChannels.get(channelUID); + if (channelToUpdate == null) { + logger.warn("Channel '{}' is either read-only or doesn't exist", channelUID); + return; + } + + switch (channelToUpdate.getRule()) { + case 1 -> updateIntRegisters(channelUID, channelToUpdate, command, Short.BYTES, IntegerValueType.UNSIGNED); + case 2 -> updateIntRegisters(channelUID, channelToUpdate, command, Short.BYTES, IntegerValueType.SIGNED); + case 3 -> + updateIntRegisters(channelUID, channelToUpdate, command, Integer.BYTES, IntegerValueType.UNSIGNED); + case 4 -> updateIntRegisters(channelUID, channelToUpdate, command, Integer.BYTES, IntegerValueType.SIGNED); + case 5 -> updateStringRegisters(channelUID, channelToUpdate, command); + case 6 -> updateRawRegisters(channelUID, channelToUpdate, command); + case 7 -> updateVersionRegisters(channelUID, channelToUpdate, command); + case 8 -> updateDateTimeRegisters(channelUID, channelToUpdate, command); + case 9 -> updateTimeRegisters(channelUID, channelToUpdate, command); + } + } + + private void updateIntRegisters(ChannelUID channelUID, ParameterItem channelToUpdate, Command command, int size, + IntegerValueType integerValueType) { + final DecimalType decimalValue = switch (command) { + case DecimalType decimal -> decimal; + case OnOffType onOff -> onOff == OnOffType.ON ? new DecimalType(1) : DecimalType.ZERO; + case StringType stringType -> { + if (channelToUpdate.getLookup().isEmpty()) { + logUnexpectedCommand(channelUID, command); + yield null; + } else { + final String lookupValue = stringType.toString(); + final Optional lookupOptional = channelToUpdate.getLookup().stream() + .filter(l -> lookupValue.equals(l.getValue())).findFirst(); + if (lookupOptional.isPresent()) { + yield new DecimalType(lookupOptional.get().getKey()); + } else { + logUnexpectedCommand(channelUID, command); + yield null; + } + } + } + default -> { + logUnexpectedCommand(channelUID, command); + yield null; + } + }; + + if (decimalValue != null) { + long value = convertNumericValue(decimalValue, channelToUpdate.getOffset(), channelToUpdate.getScale()) + .longValue(); + if (IntegerValueType.UNSIGNED == integerValueType) { + long mask = (1L << (size * 8)) - 1; + value = value & mask; + } + ByteBuffer buffer = ByteBuffer.allocate(size); + switch (size) { + case 2 -> buffer.putShort((short) value); + case 4 -> buffer.putInt((int) value); + case 8 -> buffer.putLong(value); + } + byte[] data = buffer.array(); + writeRegisters(channelToUpdate.getRegisters().getFirst(), size / 2, data); + } + } + + private void updateStringRegisters(ChannelUID channelUID, ParameterItem channelToUpdate, Command command) { + if (command instanceof StringType stringType) { + String string = stringType.toString(); + int length = (string.length() + 1) / 2 * 2; + byte[] data = ByteBuffer.allocate(length).put(string.getBytes(StandardCharsets.UTF_8)).array(); + writeRegisters(channelToUpdate.getRegisters().getFirst(), length / 2, data); + } else { + logUnexpectedCommand(channelUID, command); + } + } + + private void updateRawRegisters(ChannelUID channelUID, ParameterItem channelToUpdate, Command command) { + logger.warn("Writing Raw to logger is not implemented yet"); + } + + private void updateVersionRegisters(ChannelUID channelUID, ParameterItem channelToUpdate, Command command) { + logger.warn("Writing Version to logger is not implemented yet"); + } + + private void updateDateTimeRegisters(ChannelUID channelUID, ParameterItem channelToUpdate, Command command) { + logger.warn("Writing DateTime to logger is not implemented yet"); + } + + private void updateTimeRegisters(ChannelUID channelUID, ParameterItem channelToUpdate, Command command) { + if (command instanceof StringType string) { + String timeString = string.toString(); + final Matcher timeMatcher = TIME_PATTERN.matcher(timeString); + if (timeMatcher.matches()) { + final int hour = Integer.parseInt(timeString.substring(0, 2)); + final int minute = Integer.parseInt(timeString.substring(3, 5)); + final short value = (short) (hour * 100 + minute); + byte[] data = ByteBuffer.allocate(2).putShort(value).array(); + writeRegisters(channelToUpdate.getRegisters().getFirst(), 1, data); + } else { + logger.warn("Received string '{}' is not correct time format 'HH:mm'", timeString); + } + } else { + logUnexpectedCommand(channelUID, command); + } + } + + private BigDecimal convertNumericValue(DecimalType decimal, @Nullable BigDecimal offset, + @Nullable BigDecimal scale) { + return decimal.toBigDecimal().divide(scale != null ? scale : BigDecimal.ONE, RoundingMode.HALF_UP) + .add(offset != null ? offset : BigDecimal.ZERO); + } + + private void writeRegisters(int firstRegister, int registerCount, byte[] data) { + if (data.length > registerCount * 2) { + logger.warn( + "Data to be written ({}) is longer than the number of 2 byte registers declared for channel ({}). Data will be truncated!", + data.length, registerCount); + } + + data = ByteBuffer.wrap(data, 0, registerCount * 2).array(); + + try (SolarmanLoggerConnection solarmanLoggerConnection = solarmanLoggerConnector.createConnection()) { + logger.debug("Writing data {} to {} logger register(s) starting from 0x{}", HexUtils.bytesToHex(data), + registerCount, String.format("%04X", firstRegister)); + + if (!solarmanLoggerConnection.isConnected()) { + throw new SolarmanConnectionException("Unable to connect to logger"); + } + + if (solarmanProtocol.writeRegisters(solarmanLoggerConnection, firstRegister, data)) { + logger.info("Successfully updated registers"); + } else { + logger.error("Failed to update registers"); + } + } catch (SolarmanException e) { + logger.error("Failed to communicate with logger", e); + } + } + + private void logUnexpectedCommand(ChannelUID uid, Command command) { + logger.warn("Received unexpected command {} in channel {}", command, uid); + } +} diff --git a/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/util/ParserUtils.java b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/util/ParserUtils.java new file mode 100644 index 0000000000000..db8c68c9ea17f --- /dev/null +++ b/bundles/org.openhab.binding.solarman/src/main/java/org/openhab/binding/solarman/internal/util/ParserUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.solarman.internal.util; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link ParserUtils} contains different utility methods for parsing + * + * @author Oleksandr Mishchuk - Initial contribution + */ +@NonNullByDefault +public class ParserUtils { + private static final Pattern REGISTER_PATTERN = Pattern.compile("\\s*(0x[\\da-fA-F]+|[\\d]+)\\s*"); + + public static List parseRegisters(String registers) { + String[] tokens = registers.split(","); + return Stream.of(tokens).map(REGISTER_PATTERN::matcher).filter(Matcher::find).map(matcher -> matcher.group(1)) + .map(ParserUtils::parseNumber).toList(); + } + + public static int parseNumber(String number) { + return number.startsWith("0x") ? Integer.parseInt(number.substring(2), 16) : Integer.parseInt(number); + } +} diff --git a/bundles/org.openhab.binding.solarman/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.solarman/src/main/resources/OH-INF/thing/thing-types.xml index 02447908e62ce..f73ca35e667b1 100644 --- a/bundles/org.openhab.binding.solarman/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.solarman/src/main/resources/OH-INF/thing/thing-types.xml @@ -32,6 +32,7 @@ + diff --git a/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_hybrid.yaml b/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_hybrid.yaml index bb88f1ac21bbb..c27f6e3bfa913 100644 --- a/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_hybrid.yaml +++ b/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_hybrid.yaml @@ -597,6 +597,7 @@ parameters: uom: "" scale: 1 rule: 9 + isReadOnly: false registers: [0x00FA] icon: 'mdi:timelapse' @@ -606,6 +607,7 @@ parameters: uom: "" scale: 1 rule: 9 + isReadOnly: false registers: [0x00FB] icon: "mdi:timelapse" @@ -615,6 +617,7 @@ parameters: uom: "" scale: 1 rule: 9 + isReadOnly: false registers: [0x00FC] icon: 'mdi:timelapse' @@ -624,6 +627,7 @@ parameters: uom: "" scale: 1 rule: 9 + isReadOnly: false registers: [0x00FD] icon: 'mdi:timelapse' @@ -633,6 +637,7 @@ parameters: uom: "" scale: 1 rule: 9 + isReadOnly: false registers: [0x00FE] icon: "mdi:timelapse" @@ -642,120 +647,133 @@ parameters: uom: "" scale: 1 rule: 9 + isReadOnly: false registers: [0x00FF] icon: 'mdi:timelapse' - name: "Time of Use Power 1" - class: "" - state_class: "" - uom: "" + class: "power" + state_class: "measurement" + uom: "W" scale: 1 rule: 1 + isReadOnly: false registers: [0x0100] icon: "mdi:lightning-bolt-outline" - name: "Time of Use Power 2" - class: "" - state_class: "" - uom: "" + class: "power" + state_class: "measurement" + uom: "W" scale: 1 rule: 1 + isReadOnly: false registers: [0x0101] icon: 'mdi:lightning-bolt-outline' - name: "Time of Use Power 3" - class: "" - state_class: "" - uom: "" + class: "power" + state_class: "measurement" + uom: "W" scale: 1 rule: 1 + isReadOnly: false registers: [0x0102] icon: 'mdi:lightning-bolt-outline' - name: "Time of Use Power 4" - class: "" - state_class: "" - uom: "" + class: "power" + state_class: "measurement" + uom: "W" scale: 1 rule: 1 + isReadOnly: false registers: [0x0103] icon: 'mdi:lightning-bolt-outline' - name: "Time of Use Power 5" - class: "" - state_class: "" - uom: "" + class: "power" + state_class: "measurement" + uom: "W" scale: 1 rule: 1 + isReadOnly: false registers: [0x0104] icon: 'mdi:lightning-bolt-outline' - name: "Time of Use Power 6" - class: "" - state_class: "" - uom: "" + class: "power" + state_class: "measurement" + uom: "W" scale: 1 rule: 1 + isReadOnly: false registers: [0x0105] icon: 'mdi:lightning-bolt-outline' - name: "Time of Use SOC 1" - class: "" - state_class: "" - uom: "" + class: "battery" + state_class: "measurement" + uom: "%" scale: 1 rule: 1 + isReadOnly: false registers: [0x010C] icon: 'mdi:battery' - name: "Time of Use SOC 2" - class: "" - state_class: "" - uom: "" + class: "battery" + state_class: "measurement" + uom: "%" scale: 1 rule: 1 + isReadOnly: false registers: [0x010D] icon: 'mdi:battery' - name: "Time of Use SOC 3" - class: "" - state_class: "" - uom: "" + class: "battery" + state_class: "measurement" + uom: "%" scale: 1 rule: 1 + isReadOnly: false registers: [0x010E] icon: 'mdi:battery' - name: "Time of Use SOC 4" - class: "" - state_class: "" - uom: "" + class: "battery" + state_class: "measurement" + uom: "%" scale: 1 rule: 1 + isReadOnly: false registers: [0x010F] icon: 'mdi:battery' - name: "Time of Use SOC 5" - class: "" - state_class: "" - uom: "" + class: "battery" + state_class: "measurement" + uom: "%" scale: 1 rule: 1 + isReadOnly: false registers: [0x0110] icon: 'mdi:battery' - name: "Time of Use SOC 6" - class: "" - state_class: "" - uom: "" + class: "battery" + state_class: "measurement" + uom: "%" scale: 1 rule: 1 + isReadOnly: false registers: [0x0111] icon: 'mdi:battery' - name: "Time of Use Enable 1" class: "" - state_class: "" + state_class: "measurement" uom: "" scale: 1 rule: 1 diff --git a/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg01hp3.yaml b/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg01hp3.yaml new file mode 100644 index 0000000000000..696d62e032185 --- /dev/null +++ b/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg01hp3.yaml @@ -0,0 +1,1043 @@ +# SUN-302K-SG01HP3-EU | 30KW | Three Phase | 3 MPPT | Hybrid Inverter | High Voltage Battery + +requests: + - start: 0x0003 + end: 0x0059 + mb_functioncode: 0x03 + - start: 0x0063 + end: 0x006D + mb_functioncode: 0x03 + - start: 0x0085 + end: 0x0085 + mb_functioncode: 0x03 + - start: 0x00A6 + end: 0x00B1 + mb_functioncode: 0x03 + - start: 0x0094 + end: 0x009F + mb_functioncode: 0x03 + - start: 0x0202 + end: 0x022E + mb_functioncode: 0x03 + - start: 0x0218 + end: 0x021A + mb_functioncode: 0x03 + - start: 0x024A + end: 0x024F + mb_functioncode: 0x03 + - start: 0x0256 + end: 0x027C + mb_functioncode: 0x03 + - start: 0x0284 + end: 0x028D + mb_functioncode: 0x03 + - start: 0x0295 + end: 0x029F + mb_functioncode: 0x03 + - start: 0x02A0 + end: 0x02A8 + mb_functioncode: 0x03 + +parameters: + - group: solar + items: + - name: "PV1 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + registers: [0x02A0] + icon: "mdi:solar-power" + + - name: "PV2 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + registers: [0x02A1] + icon: "mdi:solar-power" + + - name: "PV3 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + registers: [ 0x02A2 ] + icon: "mdi:solar-power" + + - name: "PV1 Voltage" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x02A4] + icon: "mdi:solar-power" + + - name: "PV2 Voltage" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x02A6] + icon: "mdi:solar-power" + + - name: "PV3 Voltage" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [ 0x02A7 ] + icon: "mdi:solar-power" + + - name: "PV1 Current" + class: "current" + state_class: "measurement" + uom: "A" + scale: 0.1 + rule: 1 + registers: [0x02A5] + icon: "mdi:solar-power" + + - name: "PV2 Current" + class: "current" + state_class: "measurement" + uom: "A" + scale: 0.1 + rule: 1 + registers: [0x02A7] + icon: "mdi:solar-power" + + - name: "PV3 Current" + class: "current" + state_class: "measurement" + uom: "A" + scale: 0.1 + rule: 1 + registers: [ 0x02A9 ] + icon: "mdi:solar-power" + + - name: "Daily Production" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x0211] + icon: "mdi:solar-power" + validation: + max: 100 + invalidate_all: + + - name: "Total Production" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 3 + registers: [0x0216, 0x0217] + icon: "mdi:solar-power" + + - group: Battery + items: + - name: "Battery Equalization V" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.01 + rule: 1 + registers: [0x0063] + icon: "mdi:battery" + + - name: "Battery Absorption V" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.01 + rule: 1 + registers: [0x0064] + icon: "mdi:battery" + + - name: "Battery Float V" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.01 + rule: 1 + registers: [0x0065] + icon: "mdi:battery" + + - name: "Battery Capacity" + class: "battery" + state_class: "measurement" + uom: "Ah" + scale: 1 + rule: 1 + registers: [0x0066] + icon: "mdi:battery" + + - name: "Battery Empty V" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.01 + rule: 1 + registers: [0x0067] + icon: "mdi:battery" + + - name: "Battery Max A Charge" + class: "current" + state_class: "measurement" + uom: "A" + scale: 1 + rule: 1 + registers: [0x006C] + icon: "mdi:battery" + + - name: "Battery Max A Discharge" + class: "current" + state_class: "measurement" + uom: "A" + scale: 1 + rule: 1 + registers: [0x006D] + icon: "mdi:battery" + + - name: "Daily Battery Charge" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x0202] + icon: "mdi:battery-plus" + + - name: "Daily Battery Discharge" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x0203] + icon: "mdi:battery-plus" + + - name: "Total Battery Charge" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 3 + registers: [0x0204, 0x0205] + icon: "mdi:battery-plus" + + - name: "Total Battery Discharge" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 3 + registers: [0x0206, 0x0207] + icon: "mdi:battery-minus" + + - name: "Battery Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 2 + registers: [0x024E] + icon: "mdi:battery" + + - name: "Battery Voltage" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.01 + rule: 1 + registers: [0x024B] + icon: "mdi:battery" + + - name: "Battery SOC" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + registers: [0x024C] + icon: "mdi:battery" + validation: + min: 0 + max: 101 + + - name: "Battery Current" + class: "current" + state_class: "measurement" + uom: "A" + scale: 0.01 + rule: 2 + registers: [0x024F] + icon: "mdi:battery" + + - name: "Battery Temperature" + class: "temperature" + state_class: "measurement" + uom: "°C" + scale: 0.1 + rule: 1 + offset: 1000 + registers: [0x024A] + icon: "mdi:battery" + validation: + min: 1 + max: 99 + + - group: Grid + items: + - name: "Total Grid Power" + class: "measurement" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x0271] + icon: "mdi:transmission-tower" + + - name: "Grid Voltage L1" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0256] + icon: "mdi:transmission-tower" + + - name: "Grid Voltage L2" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0257] + icon: "mdi:transmission-tower" + + - name: "Grid Voltage L3" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0258] + icon: "mdi:transmission-tower" + + - name: "Internal CT L1 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x025C] + icon: "mdi:transmission-tower" + + - name: "Internal CT L2 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x025D] + icon: "mdi:transmission-tower" + + - name: "Internal CT L3 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x025E] + icon: "mdi:transmission-tower" + + - name: "External CT L1 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x0268] + icon: "mdi:transmission-tower" + + - name: "External CT L2 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x0269] + icon: "mdi:transmission-tower" + + - name: "External CT L3 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x026A] + icon: "mdi:transmission-tower" + + - name: "Daily Energy Bought" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x0208] + icon: "mdi:transmission-tower-export" + + - name: "Total Energy Bought" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x020A, 0x020B] + icon: "mdi:transmission-tower-export" + + - name: "Daily Energy Sold" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x0209] + icon: "mdi:transmission-tower-import" + + - name: "Total Energy Sold" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 3 + registers: [0x020C, 0x020D] + icon: "mdi:transmission-tower-import" + + - name: "Total Grid Production" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 4 + registers: [0x020C, 0x020D] + icon: "mdi:transmission-tower" + + - group: Upload + items: + - name: "Total Load Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x028D] + icon: "mdi:lightning-bolt-outline" + + - name: "Load L1 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x028A] + icon: "mdi:lightning-bolt-outline" + + - name: "Load L2 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x028B] + icon: "mdi:lightning-bolt-outline" + + - name: "Load L3 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x028C] + icon: "mdi:lightning-bolt-outline" + + - name: "Load Voltage L1" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0284] + icon: "mdi:lightning-bolt-outline" + + - name: "Load Voltage L2" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0285] + icon: "mdi:lightning-bolt-outline" + + - name: "Load Voltage L3" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0286] + icon: "mdi:lightning-bolt-outline" + + - name: "Daily Load Consumption" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x020E] + icon: "mdi:lightning-bolt-outline" + + - name: "Total Load Consumption" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 3 + registers: [0x020F, 0x0210] + icon: "mdi:lightning-bolt-outline" + + - group: Inverter + items: + - name: "Current L1" + class: "current" + state_class: "measurement" + uom: "A" + scale: 0.01 + rule: 2 + registers: [0x0276] + icon: "mdi:home-lightning-bolt" + + - name: "Current L2" + class: "current" + state_class: "measurement" + uom: "A" + scale: 0.01 + rule: 2 + registers: [0x0277] + icon: "mdi:home-lightning-bolt" + + - name: "Current L3" + class: "current" + uom: "A" + scale: 0.01 + rule: 2 + registers: [0x0278] + icon: "mdi:home-lightning-bolt" + + - name: "Inverter L1 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x0279] + icon: "mdi:home-lightning-bolt" + + - name: "Inverter L2 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x027A] + icon: "mdi:home-lightning-bolt" + + - name: "Inverter L3 Power" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 2 + registers: [0x027B] + icon: "mdi:home-lightning-bolt" + + - name: "DC Temperature" + class: "temperature" + state_class: "measurement" + uom: "°C" + scale: 0.1 + rule: 2 + offset: 1000 + registers: [0x021C] + icon: "mdi:thermometer" + + - name: "AC Temperature" + class: "temperature" + state_class: "measurement" + uom: "°C" + scale: 0.1 + rule: 2 + offset: 1000 + registers: [0x021D] + icon: "mdi:thermometer" + + - name: "Inverter ID" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 5 + registers: [0x0003, 0x0004, 0x0005, 0x0006, 0x0007] + isstr: true + + - name: "Communication Board Version No." + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [0x0011] + isstr: true + + - name: "Control Board Version No." + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [0x000D] + isstr: true + + - group: SmartLoad + items: + - name: "SmartLoad Enable Status" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [0x0085] + isstr: true + lookup: + - key: 0 + value: "GEN Use" + - key: 1 + value: "SMART Load output" + - key: 2 + value: "Microinverter" + icon: "mdi:lightning-bolt-outline" + + - name: "Phase voltage of Gen port A" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0295] + icon: "mdi:home-lightning-bolt" + + - name: "Phase voltage of Gen port B" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0296] + icon: "mdi:home-lightning-bolt" + + - name: "Phase voltage of Gen port C" + class: "voltage" + state_class: "measurement" + uom: "V" + scale: 0.1 + rule: 1 + registers: [0x0297] + icon: "mdi:home-lightning-bolt" + + - name: "Phase power of Gen port A" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x0298] + icon: "mdi:home-lightning-bolt" + validation: + min: 0 + max: 30000 + + - name: "Phase power of Gen port B" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x0299] + icon: "mdi:home-lightning-bolt" + validation: + min: 0 + max: 30000 + + - name: "Phase power of Gen port C" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x029A] + icon: "mdi:home-lightning-bolt" + validation: + min: 0 + max: 30000 + + - name: "Total Power of Gen port" + class: "power" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + registers: [0x029B] + icon: "mdi:home-lightning-bolt" + validation: + min: 0 + max: 30000 + + - name: "Generator daily power generation" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 1 + registers: [0x0218] + icon: "mdi:transmission-tower-import" + + - name: "Generator total power generation" + class: "energy" + state_class: "total_increasing" + uom: "kWh" + scale: 0.1 + rule: 3 + registers: [0x0219, 0x021A] + icon: "mdi:transmission-tower-import" + + - group: Alert + items: + - name: "Alert" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 6 + registers: [0x0229, 0x022A, 0x22B, 0x022C, 0x022D, 0x022E] + + - group: Time Of Use + items: + - name: "TOU Time 1" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0094] + + - name: "TOU Time 2" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0095] + + - name: "TOU Time 3" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0096] + + - name: "TOU Time 4" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0097] + + - name: "TOU Time 5" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0098] + + - name: "TOU Time 6" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0099] + + - name: "TOU Power 1" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + isReadOnly: false + registers: [0x009A] + + - name: "TOU Power 2" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + isReadOnly: false + registers: [0x009B] + + - name: "TOU Power 3" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + isReadOnly: false + registers: [0x009C] + + - name: "TOU Power 4" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + isReadOnly: false + registers: [0x009D] + + - name: "TOU Power 5" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + isReadOnly: false + registers: [0x009E] + + - name: "TOU Power 6" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 10 + rule: 1 + isReadOnly: false + registers: [0x009F] + + - name: "TOU Battery SOC 1" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A6] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 2" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A7] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 3" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A8] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 4" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A9] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 5" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00AA] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 6" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00AB] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Charge Enable 1" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [0x00AC] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 2" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00AD ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 3" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00AE ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 4" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00AF ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 5" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00B0 ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 6" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00B1 ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg04lp3.yaml b/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg04lp3.yaml index 056010702cece..af7815be5f65b 100644 --- a/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg04lp3.yaml +++ b/bundles/org.openhab.binding.solarman/src/main/resources/definitions/deye_sg04lp3.yaml @@ -11,6 +11,12 @@ requests: - start: 0x0085 end: 0x0085 mb_functioncode: 0x03 + - start: 0x0094 + end: 0x009F + mb_functioncode: 0x03 + - start: 0x00A6 + end: 0x00B1 + mb_functioncode: 0x03 - start: 0x0202 end: 0x022E mb_functioncode: 0x03 @@ -707,3 +713,305 @@ parameters: scale: 1 rule: 6 registers: [0x0229, 0x022A, 0x22B, 0x022C, 0x022D, 0x022E] + + - group: Time Of Use + items: + - name: "TOU Time 1" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0094] + + - name: "TOU Time 2" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0095] + + - name: "TOU Time 3" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0096] + + - name: "TOU Time 4" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0097] + + - name: "TOU Time 5" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0098] + + - name: "TOU Time 6" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 9 + isReadOnly: false + registers: [0x0099] + + - name: "TOU Power 1" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x009A] + + - name: "TOU Power 2" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x009B] + + - name: "TOU Power 3" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x009C] + + - name: "TOU Power 4" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x009D] + + - name: "TOU Power 5" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x009E] + + - name: "TOU Power 6" + class: "energy" + state_class: "measurement" + uom: "W" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x009F] + + - name: "TOU Battery SOC 1" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A6] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 2" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A7] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 3" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A8] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 4" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00A9] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 5" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00AA] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Battery SOC 6" + class: "battery" + state_class: "measurement" + uom: "%" + scale: 1 + rule: 1 + isReadOnly: false + registers: [0x00AB] + icon: "mdi:battery" + validation: + min: 0 + max: 100 + + - name: "TOU Charge Enable 1" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [0x00AC] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 2" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00AD ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 3" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00AE ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 4" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00AF ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 5" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00B0 ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" + + - name: "TOU Charge Enable 6" + class: "" + state_class: "" + uom: "" + scale: 1 + rule: 1 + registers: [ 0x00B1 ] + isReadOnly: false + isstr: true + lookup: + - key: 0 + value: "Charge disabled" + - key: 1 + value: "Grid charge" + - key: 2 + value: "Generator charge" + - key: 3 + value: "Grid charge + Generator charge" \ No newline at end of file diff --git a/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocolTest.java b/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocolTest.java index 594360533d085..20d4b7e9ca727 100644 --- a/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocolTest.java +++ b/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanRawProtocolTest.java @@ -45,8 +45,8 @@ class SolarmanRawProtocolTest { private SolarmanRawProtocol solarmanRawProtocol = new SolarmanRawProtocol(loggerConfiguration); @Test - void testbuildSolarmanRawFrame() { - byte[] requestFrame = solarmanRawProtocol.buildSolarmanRawFrame((byte) 0x03, 0x0063, 0x006D); + void testbuildSolarmanRawReadFrame() { + byte[] requestFrame = solarmanRawProtocol.buildSolarmanRawReadFrame((byte) 0x03, 0x0063, 11); byte[] expectedFrame = { (byte) 0x03, (byte) 0xE8, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x08, (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x63, (byte) 0x00, (byte) 0x0B, (byte) 0xF4, (byte) 0x13 }; diff --git a/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5ProtocolTest.java b/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5ProtocolTest.java index 24187834f3ca1..eb227b4214e52 100644 --- a/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5ProtocolTest.java +++ b/bundles/org.openhab.binding.solarman/src/test/java/org/openhab/binding/solarman/internal/modbus/SolarmanV5ProtocolTest.java @@ -44,9 +44,20 @@ class SolarmanV5ProtocolTest { private SolarmanV5Protocol solarmanV5Protocol = new SolarmanV5Protocol(loggerConfiguration); + @Test + void testBuildModbusReadHoldingRegistersFrame() { + byte[] modbusFrame = solarmanV5Protocol.buildModbusReadHoldingRegistersFrame((byte) 0x01, (byte) 0x03, 0x0000, + 0x0021); + byte[] expectedFrame = { (byte) 0x01, (byte) 0x03, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x21, + (byte) 0x85, (byte) 0xD2 }; + assertArrayEquals(modbusFrame, expectedFrame); + } + @Test void testbuildSolarmanV5Frame() { - byte[] requestFrame = solarmanV5Protocol.buildSolarmanV5Frame((byte) 0x03, 0x0000, 0x0020); + byte[] modbusFrame = solarmanV5Protocol.buildModbusReadHoldingRegistersFrame((byte) 0x01, (byte) 0x03, 0x0000, + 0x0021); + byte[] requestFrame = solarmanV5Protocol.buildSolarmanV5Frame(modbusFrame); byte[] expectedFrame = { (byte) 0xA5, (byte) 0x17, (byte) 0x00, (byte) 0x10, (byte) 0x45, (byte) 0x00, (byte) 0x00, (byte) 0xD2, (byte) 0x02, (byte) 0x96, (byte) 0x49, (byte) 0x02, (byte) 0x00, (byte) 0x00,