Skip to content

Commit

Permalink
Merge pull request #146 from kbialek/features/mecd
Browse files Browse the repository at this point in the history
Monitor and aggregate data from a fleet of microinverters
  • Loading branch information
kbialek authored Mar 26, 2024
2 parents 4d747e2 + 3bb9b06 commit 0adc94e
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 37 deletions.
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Do you find this project useful? Buy me a coffee [![Donate](https://img.shields.

Reads Deye solar inverter metrics using Modbus over TCP and publishes them over MQTT.

Supports single inverter installations, as well as fleet of microinverters.

## Supported inverters and metrics

The meaning of certain inverter registers depends on the inverter type.
Expand All @@ -19,15 +21,15 @@ When your inverter turns out to work well with an already exiting metrics group,

| Inverter model | Metric groups |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Deye SUN-4/5/6/7/8/10/12K-G05-P](https://www.deyeinverter.com/product/three-phase-string-inverter/sun4-5-6-7-8-10-12kg05p-412kw-three-phase-2-mppt.html) | [string](docs/metric_group_string.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN300/500G3-US-220/EU-230](https://www.deyeinverter.com/product/microinverter-1/sun300-500g3eu230.html) | [micro](docs/metric_group_micro.md), [settings_micro](docs/metric_group_settings_micro.md) |
| [Deye SUN600/800/1000G3-US-220/EU-230](https://www.deyeinverter.com/product/microinverter-1/sun600-800-1000g3eu230-single-phase-4-mppt-microinverter-rapid-shutdown.html) | [micro](docs/metric_group_micro.md), [settings_micro](docs/metric_group_settings_micro.md) |
| [Deye SUN-4/5/6/7/8/10/12K-G05-P](https://deye.com/product/sun-4-5-6-7-8-10-12k-g05-4-12kw-three-phase-2-mppt/) | [string](docs/metric_group_string.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN300/500G3-US-220/EU-230](https://deye.com/product/sun300-500g3-eu-230-300-500w-single-phase-1-mppt-micro-inverter-rapid-shutdown/) | [micro](docs/metric_group_micro.md), [settings_micro](docs/metric_group_settings_micro.md) |
| [Deye SUN600/800/1000G3-US-220/EU-230](https://deye.com/product/sun600-800-1000g3-eu-230-600-1000w-single-phase-2-mppt-micro-inverter-rapid-shutdown/) | [micro](docs/metric_group_micro.md), [settings_micro](docs/metric_group_settings_micro.md) |
| [Deye SUN-M60/80/100G3-EU-Q0](https://www.deyeinverter.com/product/microinverter-1/SUN600-800-1000G3US220-EU230-6001000W-Einphasig-2-MPPT-MikroWechselrichter-Schnelles-Herunterfahren.html) | [micro](docs/metric_group_micro.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN1300-2000G3-US-220/EU-230](https://www.deyeinverter.com/product/microinverter-1/sun13002000g3eu230.html) | [micro](docs/metric_group_micro.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN1300-2000G3-US-220/EU-230](https://deye.com/product/sun1300-2000g3-eu-230-1300-2000w-single-phase-4-mppt-micro-inverter-rapid-shutdown/) | [micro](docs/metric_group_micro.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN-5/6/8/10/12K-SG04LP3](https://deye.com/product/sun-5-6-8-10-12k-sg04lp3-5-12kw-three-phase-2-mppt-hybrid-inverter-low-voltage-battery/) | [deye_sg04lp3](docs/metric_group_deye_sg04lp3.md), [deye_sg04lp3_battery](docs/metric_group_deye_sg04lp3_battery.md), [deye_sg04lp3_ups](docs/metric_group_deye_sg04lp3_ups.md), [deye_sg04lp3_timeofuse](docs/metric_group_deye_sg04lp3_timeofuse.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN-5/6K-SG01LP1-US/EU](https://deye.com/product/sun-5-6k-sg01lp1-us-sun-7-6-8k-sg01lp1-us-eu-5-8kw-single-phase-2-mppt-hybrid-inverter-low-voltage-battery/) | [deye_hybrid](docs/metric_group_deye_hybrid.md), [deye_hybrid_battery](docs/metric_group_deye_hybrid_battery.md), [deye_hybrid_timeofuse](docs/metric_group_deye_hybrid_timeofuse.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN-7.6/8K-SG01LP1-US/EU](https://deye.com/product/sun-5-6k-sg01lp1-us-sun-7-6-8k-sg01lp1-us-eu-5-8kw-single-phase-2-mppt-hybrid-inverter-low-voltage-battery/) | [deye_hybrid](docs/metric_group_deye_hybrid.md), [deye_hybrid_battery](docs/metric_group_deye_hybrid_battery.md), [deye_hybrid_timeofuse](docs/metric_group_deye_hybrid_timeofuse.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN-25/30/40/50K-SG01HP3-EU-BM2/3/4](https://deye.com/product/sun-25-30-40-50k-sg01hp3-eu-bm2-3-4-25-50kw-three-phase-2-mppt-hybrid-inverter-low-voltage-battery/) [**(experimental)**](https://github.com/kbialek/deye-inverter-mqtt/issues/110) | [deye_sg01hp3](docs/metric_group_deye_sg01hp3.md), [deye_sg01hp3_battery](docs/metric_group_deye_sg01hp3_battery.md), [deye_sg01hp3_bms](docs/metric_group_deye_sg01hp3_bms.md), [deye_sg01hp3_ups](docs/metric_group_deye_sg01hp3_ups.md), [settings](docs/metric_group_settings.md) |
| [Deye SUN-25/30/40/50K-SG01HP3-EU-BM2/3/4](https://deye.com/product/sun-25-30-40-50k-sg01hp3-eu-bm2-3-4-25-50kw-three-phase-2-mppt-hybrid-inverter-low-voltage-battery/) | [deye_sg01hp3](docs/metric_group_deye_sg01hp3.md), [deye_sg01hp3_battery](docs/metric_group_deye_sg01hp3_battery.md), [deye_sg01hp3_bms](docs/metric_group_deye_sg01hp3_bms.md), [deye_sg01hp3_ups](docs/metric_group_deye_sg01hp3_ups.md), [settings](docs/metric_group_settings.md) |

| Meter model | Metric groups |
| ------------------------------------------------------------------- | ------------------------------------------------- |
Expand Down Expand Up @@ -59,7 +61,7 @@ The default topic name is `logger_status` and can be changed in the configuratio
The service can optionally read inverter settings. This feature may be useful when you dynamically modify active power regulation factor. Enable it by adding `settings` metric group to `DEYE_METRIC_GROUPS` env variable.

### Writing inverter settings
It is possible to modify selected inverter settings over MQTT. At the moment only active power regulation factor is supported. This feature is disabled by default.
It is possible to modify selected inverter settings over MQTT.

| Setting | Topic | Unit | Value range | Feature flag |
| ----------------------- | :------------------------------------------------------------: | ---- | :---------: | -------------------------------------- |
Expand Down Expand Up @@ -87,6 +89,28 @@ Time Of Use configuration is modified using the following workflow:
4. Alternatively send `reset` command to purge buffered modifications without writing them to the inverter.

## Additional features

### Monitoring a fleet of microinverters
This feature enables monitoring of *N* microinverters from a single service instance (docker container), which simplifies the installation and configuration.
It is designed to monitor a fleet of microinverters.
To activate this feature, set `DEYE_LOGGER_COUNT` environment variable to the number of loggers you would like to connect to. Next configure each logger by adding a set of environment variables, as follows:
```
DEYE_LOGGER_{N}_IP_ADDRESS=192.168.XXX.YYY
DEYE_LOGGER_{N}_SERIAL_NUMBER=0123456789
# Optionals
DEYE_LOGGER_{N}_PROTOCOL=at
DEYE_LOGGER_{N}_PORT=48899
DEYE_LOGGER_{N}_MAX_REG_RANGE_LENGTH_PORT=256
```
Replace `{N}` with logger index. All loggers in the range of 1 to `DEYE_LOGGER_COUNT` must be configured.

All other configuration options, in particular the metric groups, are shared by all configured loggers. For example, if you set `DEYE_FEATURE_SET_TIME=true`, it will activate set-time feature for all configured loggers.

Each logger gets its own MQTT topic prefix `{MQTT_TOPIC_PREFIX}/{N}`

Additionally, you can enable multi-inverter data aggregation. Set `DEYE_FEATURE_MULTI_INVERTER_DATA_AGGREGATOR=true` to compute and report `Aggregated daily energy` and `Aggregated AC active power` for the entire fleet. See [aggregated metrics](docs/metric_group_aggregated.md)


### Automatically set logger/inverter time
Monitors current logger status and sets the time at the logger/inverter once the connection to it can be established.
This is useful in a setup where the inverter has no access to the public internet, or is cut off from the Solarman cloud services.
Expand Down Expand Up @@ -231,15 +255,16 @@ All configuration options are controlled through environment variables.
* `settings` - inverter settings, all types except micro
* `settings_micro` - inverter settings for micro inverters
* `DEYE_LOGGER_COUNT` - declares the number of inverters, and therefore loggers to connect, optional, defaults to `0`, which means, that multi-inverter support is disabled
* `DEYE_LOGGER_SERIAL_NUMBER` - inverter data logger serial number
* `DEYE_LOGGER_IP_ADDRESS` - inverter data logger IP address
* `DEYE_LOGGER_PORT` - inverter data logger communication port, optional, defaults to 8899 for Modbus/TCP, and 48899 for Modbus/AT
* `DEYE_LOGGER_PROTOCOL` - inverter communication protocol, optional, either `tcp` for Modbus/TCP, or `at` for Modbus/AT, defaults to `tcp`
* `DEYE_LOGGER_MAX_REG_RANGE_LENGTH` - controls maximum number of registers to be read in a single Modbus registers read operation, defaults to 256
* `DEYE_LOGGER_SERIAL_NUMBER` or `DEYE_LOGGER_{N}_SERIAL_NUMBER` - inverter data logger serial number
* `DEYE_LOGGER_IP_ADDRESS` or `DEYE_LOGGER_{N}_IP_ADDRESS` - inverter data logger IP address
* `DEYE_LOGGER_PORT` or `DEYE_LOGGER_{N}_PORT` - inverter data logger communication port, optional, defaults to 8899 for Modbus/TCP, and 48899 for Modbus/AT
* `DEYE_LOGGER_PROTOCOL` or `DEYE_LOGGER_{N}_PROTOCOL` - inverter communication protocol, optional, either `tcp` for Modbus/TCP, or `at` for Modbus/AT, defaults to `tcp`
* `DEYE_LOGGER_MAX_REG_RANGE_LENGTH` or `DEYE_LOGGER_{N}_MAX_REG_RANGE_LENGTH`- controls maximum number of registers to be read in a single Modbus registers read operation, defaults to 256
* `DEYE_FEATURE_MQTT_PUBLISHER` - controls, if the service will publish metrics over mqtt, defaults to `true`
* `DEYE_FEATURE_SET_TIME` - when set to `true`, the service will automatically set the inverter/logger time, defaults to `false`
* `DEYE_FEATURE_ACTIVE_POWER_REGULATION` - enables active power regulation control over MQTT command topic
* `DEYE_FEATURE_TIME_OF_USE` - enabled Time Of Use feature control over MQTT
* `DEYE_FEATURE_TIME_OF_USE` - enables Time Of Use feature control over MQTT
* `DEYE_FEATURE_MULTI_INVERTER_DATA_AGGREGATOR` - enables multi-inverter data aggregation and publishing
* `MQTT_HOST` - MQTT Broker IP address
* `MQTT_PORT` - MQTT Broker port, , defaults to `1883`
* `MQTT_USERNAME` - MQTT Broker username for authentication, defaults to `None`
Expand Down
11 changes: 11 additions & 0 deletions config.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,14 @@ DEYE_METRIC_GROUPS=string
# DEYE_FEATURE_ACTIVE_POWER_REGULATION=false
# DEYE_FEATURE_SET_TIME=false

## Sample multiinverter configuration with two loggers
#
# DEYE_LOGGER_COUNT=2

# DEYE_LOGGER_1_IP_ADDRESS=192.168.0.1
# DEYE_LOGGER_1_SERIAL_NUMBER=1234567890
# DEYE_LOGGER_1_PROTOCOL=at

# DEYE_LOGGER_2_IP_ADDRESS=192.168.0.2
# DEYE_LOGGER_2_SERIAL_NUMBER=1234567891
# DEYE_LOGGER_2_PROTOCOL=at
1 change: 1 addition & 0 deletions plugins/deye_plugin_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def get_id(self):
return "sample_publisher"

def process(self, events: DeyeEventList):
print(f"Processing events from logger: {events.logger_index}")
for event in events:
if isinstance(event, DeyeObservationEvent):
observation_event: DeyeObservationEvent = event
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ pyyaml==6.0 ; python_version >= "3.10" and python_version < "4.0" \
setuptools==67.8.0 ; python_version >= "3.10" and python_version < "4.0" \
--hash=sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f \
--hash=sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102
tomli==2.0.1 ; python_version >= "3.10" and python_version < "3.11" \
tomli==2.0.1 ; python_version >= "3.10" and python_full_version <= "3.11.0a6" \
--hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
--hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
virtualenv==20.23.0 ; python_version >= "3.10" and python_version < "4.0" \
Expand Down
40 changes: 26 additions & 14 deletions src/deye_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.

import sys
import logging

from deye_config import DeyeConfig
from deye_connector_factory import DeyeConnectorFactory
Expand All @@ -24,41 +25,52 @@

class DeyeCli:
def __init__(self, config: DeyeConfig):
connector = DeyeConnectorFactory().create_connector(config.logger)
self.__modbus = DeyeModbus(connector)
self.__log = logging.getLogger()
self.__config = config

def exec_command(self, args):
logger_config = self.__config.logger
if args[0].isnumeric():
logger_index = int(args[0]) - 1
logger_config = self.__config.logger_configs[logger_index]
args = args[1:]

self.__log.info(f"Connecting to logger at IP {logger_config.ip_address}")

connector = DeyeConnectorFactory().create_connector(logger_config)
modbus = DeyeModbus(connector)

command = args[0]
if command == "r":
self.read_register(args[1:])
self.read_register(modbus, args[1:])
elif command == "w":
self.write_register(args[1:])
self.write_register(modbus, args[1:])

def read_register(self, args):
def read_register(self, modbus, args):
reg_address = int(args[0])
registers = self.__modbus.read_registers(reg_address, reg_address)
registers = modbus.read_registers(reg_address, reg_address)
if registers is None:
print("Error: no registers read")
self.__log.error("Error: no registers read")
sys.exit(1)
if reg_address not in registers:
print(f"Error: register {reg_address} not read")
self.__log.error(f"Error: register {reg_address} not read")
sys.exit(1)
reg_bytes = registers[reg_address]
reg_value_int = int.from_bytes(reg_bytes, "big")
low_byte = reg_bytes[1]
high_byte = reg_bytes[0]
print(f"int: {reg_value_int}, l: {low_byte}, h: {high_byte}")
self.__log.info(f"int: {reg_value_int}, l: {low_byte}, h: {high_byte}")

def write_register(self, args):
def write_register(self, modbus, args):
if len(args) < 2:
print("Not enough arguments")
self.__log.error("Not enough arguments")
sys.exit(1)
reg_address = int(args[0])
reg_value = int(args[1])
if self.__modbus.write_register_uint(reg_address, reg_value):
print("Ok")
if modbus.write_register_uint(reg_address, reg_value):
self.__log.info("Ok")
else:
print("Error")
self.__log.error("Error")


def main():
Expand Down
5 changes: 3 additions & 2 deletions src/deye_mqtt_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@

from deye_events import DeyeEventList, DeyeEventProcessor, DeyeLoggerStatusEvent, DeyeObservationEvent
from deye_mqtt import DeyeMqttClient, DeyeMqttPublishError
from deye_config import DeyeLoggerConfig


class DeyeMqttPublisher(DeyeEventProcessor):
"""
Publishes events over MQTT.
"""

def __init__(self, mqtt_client: DeyeMqttClient):
self.__log = logging.getLogger(DeyeMqttPublisher.__name__)
def __init__(self, logger_config: DeyeLoggerConfig, mqtt_client: DeyeMqttClient):
self.__log = logger_config.logger_adapter(logging.getLogger(DeyeMqttPublisher.__name__))
self.__mqtt_client = mqtt_client

def initialize(self):
Expand Down
8 changes: 4 additions & 4 deletions src/deye_processor_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ def __create_builtin_processors(
self, logger_config: DeyeLoggerConfig, modbus: DeyeModbus, sensors: list[Sensor]
) -> list[DeyeEventProcessor]:
processors = []
self.__append_processor(processors, DeyeMqttPublisher(self.__mqtt_client))
self.__append_processor(processors, DeyeSetTimeProcessor(modbus))
self.__append_processor(processors, DeyeMqttPublisher(logger_config, self.__mqtt_client))
self.__append_processor(processors, DeyeSetTimeProcessor(logger_config, modbus))
self.__append_processor(processors, DeyeTimeOfUseService(logger_config, self.__mqtt_client, sensors, modbus))
self.__append_processor(
processors, DeyeActivePowerRegulationEventProcessor(logger_config, self.__mqtt_client, modbus)
processors, DeyeActivePowerRegulationEventProcessor(logger_config, self.__mqtt_client, sensors, modbus)
)
return processors

Expand All @@ -74,7 +74,7 @@ def create_aggregating_processors(self, logger_config: DeyeLoggerConfig) -> list

def __create_builtin_aggregating_processors(self, logger_config: DeyeLoggerConfig) -> list[DeyeEventProcessor]:
processors = []
self.__append_processor(processors, DeyeMqttPublisher(self.__mqtt_client))
self.__append_processor(processors, DeyeMqttPublisher(logger_config, self.__mqtt_client))
return processors

def __append_processor(self, processors: list[DeyeEventProcessor], processor: DeyeEventProcessor):
Expand Down
5 changes: 3 additions & 2 deletions src/deye_set_time_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@

from deye_events import DeyeEventList, DeyeEventProcessor
from deye_modbus import DeyeModbus
from deye_config import DeyeLoggerConfig


class DeyeSetTimeProcessor(DeyeEventProcessor):
"""
Set logger time when the logger becomes available online.
"""

def __init__(self, modbus: DeyeModbus):
self.__log = logging.getLogger(DeyeSetTimeProcessor.__name__)
def __init__(self, logger_config: DeyeLoggerConfig, modbus: DeyeModbus):
self.__log = logger_config.logger_adapter(logging.getLogger(DeyeSetTimeProcessor.__name__))
self.__modbus = modbus
self.__last_status = False
self.__last_update_ts = datetime.min
Expand Down
Loading

0 comments on commit 0adc94e

Please sign in to comment.