Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,10 @@ EOS Connect helps you get the most out of your solar and storage systems—wheth
- Monitors and controls EVCC charging modes and states.
- Supports fast charge, PV charging, and combined modes.
- **Inverter Interfaces**:
- OPTION 1: Communicates directly with a Fronius GEN24 to monitor and control energy flows.
- OPTION 1: Communicates directly with Fronius GEN24 or Victron inverters to monitor and control energy flows.
- `fronius_gen24`: Enhanced interface with firmware-based authentication for all firmware versions
- `fronius_gen24_legacy`: Legacy interface for corner cases or troubleshooting
- `victron`: Full support for Victron Energy inverters
- OPTION 2: Use the [evcc external battery control](https://docs.evcc.io/docs/integrations/rest-api) to interact with all inverter/ battery systems that [are supported by evcc](https://docs.evcc.io/en/docs/devices/meters) (hint: the dynamic max charge power is currently not supported by evcc external battery control)
- OPTION 3: using without a direct control interface to get the resulting commands by **EOS connect** MQTT or web API to control within your own environment (e.g. [Integrate inverter e.g. sungrow SH10RT #35](https://github.com/ohAnd/EOS_connect/discussions/35) )
- Retrieves real-time data such as grid charge power, discharge power, and battery SOC.
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ paho-mqtt>=2.1.0
packaging>=23.2
# pvlib>=0.13.0
open-meteo-solar-forecast>=0.1.22
psutil>=7.0.0
psutil>=7.0.0
pymodbus>=3.11.4
17 changes: 9 additions & 8 deletions src/CONFIG_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,17 +327,18 @@ Refer to this table and details when editing your `config.yaml` and for troubles
Specifies the type of inverter. Possible values:
- `fronius_gen24`: Use the Fronius Gen24 inverter (enhanced V2 interface with firmware-based authentication for all firmware versions).
- `fronius_gen24_legacy`: Use the Fronius Gen24 inverter (legacy V1 interface for corner cases).
- `victron`: Use Victron Energy inverters.
- `evcc`: Use the universal interface via evcc external battery control (evcc config below has to be valid).
- `default`: Disable inverter control (only display the target state).

- **`inverter.address`**:
The IP address of the inverter. (only needed for fronius_gen24/fronius_gen24_legacy)
The IP address of the inverter. (only needed for fronius_gen24/fronius_gen24_legacy/victron)

- **`inverter.user`**:
The username for the inverter's local portal. (only needed for fronius_gen24/fronius_gen24_legacy)
The username for the inverter's local portal. (only needed for fronius_gen24/fronius_gen24_legacy/victron)

- **`inverter.password`**:
The password for the inverter's local portal. (only needed for fronius_gen24/fronius_gen24_legacy)
The password for the inverter's local portal. (only needed for fronius_gen24/fronius_gen24_legacy/victron)

**Note for enhanced interface**: The default `fronius_gen24` interface automatically detects your firmware version and uses the appropriate authentication method. If you recently updated your inverter firmware to 1.38.6-1+ or newer, you may need to reset your password in the WebUI (http://your-inverter-ip/) under Settings -> User Management. New firmware versions require password reset after updates to enable the improved encryption method.

Expand Down Expand Up @@ -491,10 +492,10 @@ pv_forecast:
resource_id: "" # Resource ID for Solcast (required only when source is 'solcast')
# Inverter configuration
inverter:
type: default # Type of inverter - fronius_gen24, fronius_gen24_legacy, evcc, default (default will disable inverter control - only displaying the target state) - preset: default
address: 192.168.1.12 # Address of the inverter (fronius_gen24, fronius_gen24_legacy only)
user: customer # Username for the inverter (fronius_gen24, fronius_gen24_legacy only)
password: abc123 # Password for the inverter (fronius_gen24, fronius_gen24_legacy only)
type: default # Type of inverter - fronius_gen24, fronius_gen24_legacy, victron, evcc, default (default will disable inverter control - only displaying the target state) - preset: default
address: 192.168.1.12 # Address of the inverter (fronius_gen24, fronius_gen24_legacy, victron only)
user: customer # Username for the inverter (fronius_gen24, fronius_gen24_legacy, victron only)
password: abc123 # Password for the inverter (fronius_gen24, fronius_gen24_legacy, victron only)
max_grid_charge_rate: 5000 # Max inverter grid charge rate in W - default: 5000
max_pv_charge_rate: 5000 # Max imverter PV charge rate in W - default: 5000
# EVCC configuration
Expand Down Expand Up @@ -567,7 +568,7 @@ pv_forecast:
horizon: 10,20,10,15 # Horizon to calculate shading up to 360 values to describe shading situation for your PV.
# Inverter configuration
inverter:
type: default # Type of inverter - fronius_gen24, fronius_gen24_legacy, evcc, default (default will disable inverter control - only displaying the target state) - preset: default
type: default # Type of inverter - fronius_gen24, fronius_gen24_legacy, victron, evcc, default (default will disable inverter control - only displaying the target state) - preset: default
max_grid_charge_rate: 5000 # Max inverter grid charge rate in W - default: 5000
max_pv_charge_rate: 5000 # Max imverter PV charge rate in W - default: 5000
# EVCC configuration
Expand Down
77 changes: 16 additions & 61 deletions src/eos_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,16 @@
from interfaces.base_control import BaseControl
from interfaces.load_interface import LoadInterface
from interfaces.battery_interface import BatteryInterface
from interfaces.inverter_fronius import FroniusWR
from interfaces.inverter_fronius_v2 import FroniusWRV2
from interfaces.evcc_interface import EvccInterface
from interfaces.optimization_interface import OptimizationInterface
from interfaces.price_interface import PriceInterface
from interfaces.mqtt_interface import MqttInterface
from interfaces.pv_interface import PvInterface
from interfaces.port_interface import PortInterface

from interfaces.inverters import create_inverter, BaseInverter


# Check Python version early
if sys.version_info < (3, 11):
sys.stderr.write(
Expand Down Expand Up @@ -137,60 +138,10 @@ def formatTime(self, record, datefmt=None):

# initialize base control
base_control = BaseControl(config_manager.config, time_zone, time_frame_base)
# initialize the inverter interface
inverter_interface = None

# Handle backward compatibility for old interface names
inverter_type = config_manager.config["inverter"]["type"]
if inverter_type == "fronius_gen24_v2":
logger.warning(
"[Config] Interface name 'fronius_gen24_v2' is deprecated. "
"Please update your config.yaml to use 'fronius_gen24' instead. "
"Using enhanced interface for compatibility."
)
inverter_type = "fronius_gen24" # Auto-migrate to new name

if inverter_type == "fronius_gen24":
# Enhanced V2 interface (default for existing users)
logger.info(
"[Inverter] Using enhanced Fronius GEN24 interface with firmware-based authentication"
)
inverter_config = {
"address": config_manager.config["inverter"]["address"],
"max_grid_charge_rate": config_manager.config["inverter"][
"max_grid_charge_rate"
],
"max_pv_charge_rate": config_manager.config["inverter"]["max_pv_charge_rate"],
"user": config_manager.config["inverter"]["user"],
"password": config_manager.config["inverter"]["password"],
}
inverter_interface = FroniusWRV2(inverter_config)
elif inverter_type == "fronius_gen24_legacy":
# Legacy V1 interface (for corner cases)
logger.info(
"[Inverter] Using legacy Fronius GEN24 interface (V1) for compatibility"
)
inverter_config = {
"address": config_manager.config["inverter"]["address"],
"max_grid_charge_rate": config_manager.config["inverter"][
"max_grid_charge_rate"
],
"max_pv_charge_rate": config_manager.config["inverter"]["max_pv_charge_rate"],
"user": config_manager.config["inverter"]["user"],
"password": config_manager.config["inverter"]["password"],
}
inverter_interface = FroniusWR(inverter_config)
elif inverter_type == "evcc":
logger.info(
"[Inverter] Inverter type %s - using the universal evcc external battery control.",
inverter_type,
)
else:
logger.info(
"[Inverter] Inverter type %s - no external connection."
+ " Changing to show only mode.",
config_manager.config["inverter"]["type"],
)
# Initialize the inverter interface via factory
inverter_interface = create_inverter(config_manager.config["inverter"])
inverter_interface.initialize()


# callback function for evcc interface
Expand Down Expand Up @@ -1034,7 +985,7 @@ def __update_state_loop_data_loop(self):
self.__start_update_service_data_loop()

def __run_data_loop(self):
if inverter_type in ["fronius_gen24", "fronius_gen24_legacy"]:
if inverter_interface.supports_extended_monitoring():
inverter_interface.fetch_inverter_data()
mqtt_interface.update_publish_topics(
{
Expand Down Expand Up @@ -1123,10 +1074,15 @@ def change_control_state():
"""
inverter_fronius_en = False
inverter_evcc_en = False
if inverter_type in ["fronius_gen24", "fronius_gen24_legacy"]:
inverter_fronius_en = True
elif config_manager.config["inverter"]["type"] == "evcc":
# Check if we're using EVCC external battery control first
if config_manager.config["inverter"]["type"] == "evcc":
inverter_evcc_en = True
# Then check if we have a real hardware inverter (not NullInverter for "default")
elif (
isinstance(inverter_interface, BaseInverter)
and config_manager.config["inverter"]["type"] != "default"
):
inverter_fronius_en = True

current_overall_state = base_control.get_current_overall_state_number()
current_overall_state_text = base_control.get_current_overall_state()
Expand Down Expand Up @@ -1469,8 +1425,7 @@ def get_controls():
"inverter": {
"inverter_special_data": (
inverter_interface.get_inverter_current_data()
if inverter_type in ["fronius_gen24", "fronius_gen24_legacy"]
and inverter_interface is not None
if inverter_interface.supports_extended_monitoring()
else None
)
},
Expand Down
149 changes: 149 additions & 0 deletions src/interfaces/inverter_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@

"""Base inverter interface definitions.

This module provides the Abstract Base Class for inverter implementations
used by EOS_connect, including common helpers and utility methods.
"""

from abc import ABC, abstractmethod
import logging

logger = logging.getLogger("__main__").getChild("BaseInverter")
logger.setLevel(logging.INFO)


class BaseInverter(ABC):
"""Abstract base class for different inverter types."""

def __init__(self, config: dict):
# Store complete config (for tests & future extensions)
self.config = config

# Extract common values as well
self.address = config.get("address")
self.user = config.get("user", "customer").lower()
self.password = config.get("password", "")
self.max_grid_charge_rate = config.get("max_grid_charge_rate")
self.max_pv_charge_rate = config.get("max_pv_charge_rate")

self.is_authenticated = False
self.inverter_type = self.__class__.__name__

logger.info(f"[{self.inverter_type}] Initialized for {self.address}")

# --- Optionale Authentifizierung ---

@abstractmethod
def initialize(self):
"""Heavy initialization (API calls)."""
raise NotImplementedError()

def authenticate(self) -> bool:
"""
Optional authentication.
By default, this method does nothing and returns True.
Subclasses can override it if authentication is needed.
"""
logger.debug(f"[{self.inverter_type}] No authentication required")
self.is_authenticated = True
return True

# --- Mandatory Methods for All Inverters ---

@abstractmethod
def set_battery_mode(self, mode: str) -> bool:
"""Sets the battery mode (e.g., normal, hold, charge)."""
raise NotImplementedError()

# --- EOS Connect Helpers ---

@abstractmethod
def set_mode_avoid_discharge(self) -> bool:
"""Prevents battery discharge (Hold Mode)."""
return self.set_battery_mode("hold")

@abstractmethod
def set_mode_allow_discharge(self) -> bool:
"""Allows battery discharge (Normal Mode)."""
return self.set_battery_mode("normal")

@abstractmethod
def set_allow_grid_charging(self, value: bool):
"""Enable or disable charging from the grid."""
raise NotImplementedError()

@abstractmethod
def get_battery_info(self) -> dict:
"""Reads current battery information."""
raise NotImplementedError()

@abstractmethod
def fetch_inverter_data(self) -> dict:
"""Reads current inverter data."""
raise NotImplementedError()

@abstractmethod
def set_mode_force_charge(self, charge_power_w: int) -> bool:
"""
Force charge mode with specific power.
Each subclass must implement this method.
"""
raise NotImplementedError()

@abstractmethod
def connect_inverter(self) -> bool:
"""
Establishes a connection to the inverter.

This method is required to be implemented by all subclasses.
It should return True if the connection was successful, False otherwise.
"""
raise NotImplementedError()

@abstractmethod
def disconnect_inverter(self) -> bool:
"""
Disconnect from the inverter.

This method is required to be implemented by all subclasses.
It should return True if the disconnection was successful, False otherwise.
"""
raise NotImplementedError()

# --- Common Utility Methods ---

def api_set_max_pv_charge_rate(self, max_pv_charge_rate: int):
"""
Set the maximum PV charge rate (optional, inverter-specific).

This method is specific to certain inverters (e.g., Fronius) that support
dynamic PV charge rate limiting. Default implementation is a no-op.
Override in subclasses that support this feature.

Args:
max_pv_charge_rate: Maximum charge rate from PV in watts
"""
logger.debug(
"[%s] api_set_max_pv_charge_rate(%d W) not implemented (no-op)",
self.inverter_type,
max_pv_charge_rate,
)

def supports_extended_monitoring(self) -> bool:
"""
Indicates whether this inverter supports extended monitoring data
(temperature sensors, fan control, etc.).

Returns:
False by default. Subclasses providing extended monitoring
should override this method to return True.
"""
return False

def disconnect(self):
"""Session closes itself."""
logger.info(f"[{self.inverter_type}] Session closed")

def shutdown(self):
"""Standard shutdown (can be overridden)."""
self.disconnect()
Loading
Loading