Skip to content

Commit 5a094e6

Browse files
authored
Merge pull request #1 from deKibi/feature/backup-manual-power-mode-swtich
Feature/backup manual power mode swtich
2 parents cf955d0 + b25a239 commit 5a094e6

8 files changed

Lines changed: 649 additions & 12 deletions

File tree

.env.example

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# CONFIGURATION
2+
MUST_PORT=COM3
3+
4+
# MYSQL DATABASE
5+
MYSQL_HOST='db_ip'
6+
MYSQL_DATABASE='db_name'
7+
MYSQL_USER='db_user'
8+
MYSQL_PASSWORD='user_password'
9+
10+
# DATA GATHERING
11+
# Interval between inverter data requests, in seconds.
12+
# Minimum: 10 seconds
13+
# Maximum: 3600 seconds
14+
# Invalid or missing values will use the default of 60 seconds.
15+
DATA_GATHER_INTERVAL_SECONDS=60
16+
17+
# POWER MODE AUTO SWITCH
18+
# Allowed values: true / false
19+
ENABLE_AUTO_SWITCH=false
20+
21+
# Format: HH:MM
22+
# 19:59 - False, 20:00 - True, 20:01 - True, 20:02 - True
23+
AUTO_SWITCH_TARGET_TIME=20:00
24+
25+
# Target inverter energy mode
26+
# Allowed values: SBU, SUB, UTI, SOL
27+
AUTO_SWITCH_TARGET_MODE=SUB
28+
29+
# POWER MODE AUTO SWITCH — GRID OUTAGE
30+
# Automatically switch the inverter mode when the electrical grid is unavailable.
31+
# Allowed values: true / false
32+
ENABLE_GRID_OUTAGE_AUTO_SWITCH=false
33+
34+
# Target inverter mode when the electrical grid is unavailable.
35+
# Allowed values: SBU, SUB, UTI, SOL
36+
GRID_OUTAGE_TARGET_MODE=SUB

config.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# config.py
2+
3+
# Standard Libraries
4+
import os
5+
from datetime import datetime, time
6+
from enum import IntEnum
7+
from typing import Final
8+
9+
# Third-Party Libraries
10+
from dotenv import load_dotenv
11+
12+
13+
load_dotenv()
14+
15+
16+
class EnergyMode(IntEnum):
17+
"""
18+
Inverter energy mode codes.
19+
20+
Replace the codes below with the actual codes used by the inverter.
21+
"""
22+
23+
# 1 = SBU, 2 = SUB, 3 = UTI, 4 = SOL
24+
SBU = 1
25+
SUB = 2
26+
UTI = 3
27+
SOL = 4
28+
29+
def get_env_bool(variable_name: str, default: bool = False) -> bool:
30+
"""
31+
Read and convert a boolean environment variable.
32+
"""
33+
value = os.getenv(variable_name)
34+
35+
if value is None:
36+
return default
37+
38+
normalized_value = value.strip().lower()
39+
40+
if normalized_value in {"true", "1", "yes", "on"}:
41+
return True
42+
43+
if normalized_value in {"false", "0", "no", "off"}:
44+
return False
45+
46+
raise ValueError(
47+
f"Environment variable {variable_name} must contain true or false"
48+
)
49+
50+
51+
def get_env_int(
52+
variable_name: str,
53+
default: int,
54+
min_value: int | None = None,
55+
max_value: int | None = None,
56+
) -> int:
57+
"""
58+
Read an integer environment variable.
59+
60+
Returns the default value if the variable is missing
61+
or contains an invalid value.
62+
63+
Optionally limits the value to a minimum and maximum.
64+
"""
65+
raw_value = os.getenv(variable_name)
66+
67+
if raw_value is None:
68+
return default
69+
70+
try:
71+
value = int(raw_value.strip())
72+
except (TypeError, ValueError):
73+
print(
74+
f"Invalid value for {variable_name}: {raw_value!r}. "
75+
f"Using default value: {default}."
76+
)
77+
return default
78+
79+
if min_value is not None and value < min_value:
80+
print(
81+
f"{variable_name} is below the minimum value "
82+
f"of {min_value}. Using {min_value}."
83+
)
84+
return min_value
85+
86+
if max_value is not None and value > max_value:
87+
print(
88+
f"{variable_name} is above the maximum value "
89+
f"of {max_value}. Using {max_value}."
90+
)
91+
return max_value
92+
93+
return value
94+
95+
96+
def get_env_time(variable_name: str, default: str) -> time:
97+
"""
98+
Read an environment variable in HH:MM format and convert it to time.
99+
"""
100+
value = os.getenv(variable_name, default)
101+
102+
try:
103+
return datetime.strptime(value, "%H:%M").time()
104+
except ValueError as error:
105+
raise ValueError(
106+
f"Environment variable {variable_name} "
107+
f"must use the HH:MM format"
108+
) from error
109+
110+
111+
def get_env_energy_mode(
112+
variable_name: str,
113+
default: EnergyMode,
114+
) -> EnergyMode:
115+
"""
116+
Read an energy mode name and convert it to EnergyMode.
117+
"""
118+
value = os.getenv(variable_name, default.name).strip().upper()
119+
120+
try:
121+
return EnergyMode[value]
122+
except KeyError as error:
123+
allowed_modes = ", ".join(mode.name for mode in EnergyMode)
124+
125+
raise ValueError(
126+
f"Environment variable {variable_name} contains "
127+
f"unsupported mode {value}. Allowed modes: {allowed_modes}"
128+
) from error
129+
130+
131+
MUST_PORT: Final[str] = os.getenv("MUST_PORT", "COM3")
132+
133+
DATA_GATHER_INTERVAL_SECONDS: Final[int] = get_env_int(
134+
variable_name="DATA_GATHER_INTERVAL_SECONDS",
135+
default=60,
136+
min_value=10,
137+
max_value=3600,
138+
)
139+
140+
ENABLE_AUTO_SWITCH: Final[bool] = get_env_bool(
141+
variable_name="ENABLE_AUTO_SWITCH",
142+
default=False,
143+
)
144+
145+
AUTO_SWITCH_TARGET_TIME: Final[time] = get_env_time(
146+
variable_name="AUTO_SWITCH_TARGET_TIME",
147+
default="20:00",
148+
)
149+
150+
AUTO_SWITCH_TARGET_MODE: Final[EnergyMode] = get_env_energy_mode(
151+
variable_name="AUTO_SWITCH_TARGET_MODE",
152+
default=EnergyMode.SUB,
153+
)
154+
155+
# Grid outage automatic mode switch
156+
ENABLE_GRID_OUTAGE_AUTO_SWITCH: Final[bool] = (
157+
get_env_bool(
158+
variable_name="ENABLE_GRID_OUTAGE_AUTO_SWITCH",
159+
default=False,
160+
)
161+
)
162+
163+
GRID_OUTAGE_TARGET_MODE: Final[EnergyMode] = (
164+
get_env_energy_mode(
165+
variable_name="GRID_OUTAGE_TARGET_MODE",
166+
default=EnergyMode.SUB,
167+
)
168+
)
169+
170+
171+
if __name__ == '__main__':
172+
print("debugging config.py")
173+
174+
# if (
175+
# ENABLE_AUTO_SWITCH
176+
# and is_time_reached(AUTO_SWITCH_TARGET_TIME)
177+
# and current_energy_mode != AUTO_SWITCH_TARGET_MODE.value
178+
# ):
179+
# switch_energy_mode(AUTO_SWITCH_TARGET_MODE.value)
180+
181+
print("Enable auto switch:", ENABLE_AUTO_SWITCH)
182+
print("Object Type:", type(ENABLE_AUTO_SWITCH))
183+
184+
auto_switch_target_mode_name = AUTO_SWITCH_TARGET_MODE.name
185+
print("Auto switch target mode name:", auto_switch_target_mode_name)
186+
print("Object Type:", type(auto_switch_target_mode_name))
187+
188+
auto_switch_target_mode_code = AUTO_SWITCH_TARGET_MODE.value
189+
print("Auto switch target mode code:", auto_switch_target_mode_code)
190+
print("Object Type:", type(auto_switch_target_mode_code))
191+
192+
grid_outage_auto_switch = ENABLE_GRID_OUTAGE_AUTO_SWITCH
193+
print("Grid outage auto switch:", grid_outage_auto_switch)
194+
print("Object Type:", type(grid_outage_auto_switch))
195+
196+
grid_outage_target_mode_name = GRID_OUTAGE_TARGET_MODE.name
197+
grid_outage_target_mode_code = GRID_OUTAGE_TARGET_MODE.value
198+
print("Grid outage target mode:", grid_outage_target_mode_name)
199+
print("Object Type:", type(grid_outage_target_mode_name))
200+
print("Grid outage target mode code:", grid_outage_target_mode_code)
201+
print("Object Type:", type(grid_outage_target_mode_code))

energy_mode_control/__init__.py

Whitespace-only changes.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# energy_mode_control/energy_mode_controller.py
2+
3+
# Standard Libraries
4+
from typing import Any, Final
5+
6+
# Custom Modules
7+
from config import (
8+
ENABLE_AUTO_SWITCH,
9+
AUTO_SWITCH_TARGET_MODE,
10+
AUTO_SWITCH_TARGET_TIME,
11+
ENABLE_GRID_OUTAGE_AUTO_SWITCH,
12+
GRID_OUTAGE_TARGET_MODE,
13+
EnergyMode
14+
)
15+
from energy_mode_control.time_utils import is_time_reached
16+
from energy_mode_control.energy_mode_switcher import (
17+
switch_energy_mode,
18+
)
19+
20+
21+
# Grid availability
22+
GRID_OUTAGE_VOLTAGE_THRESHOLD: Final[float] = 10.0
23+
24+
25+
def handle_energy_mode_control(must_data: dict[str, Any] | None) -> bool:
26+
try:
27+
return _handle_energy_mode_control(must_data)
28+
except Exception as e:
29+
print("Failed to handle energy mode control:", e)
30+
return False
31+
32+
33+
def _handle_energy_mode_control(must_data: dict[str, Any] | None) -> bool:
34+
if not ENABLE_AUTO_SWITCH and not ENABLE_GRID_OUTAGE_AUTO_SWITCH:
35+
print("All automatic energy mode control features are disabled, exiting.")
36+
return False
37+
38+
if must_data is None:
39+
print("must_data is None - cannot control energy mode.")
40+
return False
41+
42+
# Further business logic will be here.
43+
if not isinstance(must_data, dict):
44+
print("must_data has invalid type.")
45+
return False
46+
47+
current_energy_mode_raw = must_data.get("EnergyUseMode")
48+
grid_voltage_raw = must_data.get("GridVoltage")
49+
50+
if current_energy_mode_raw is None:
51+
print("EnergyUseMode is missing from must_data.")
52+
return False
53+
54+
if grid_voltage_raw is None:
55+
print("GridVoltage is missing from must_data.")
56+
return False
57+
58+
try:
59+
current_energy_mode = EnergyMode(
60+
int(current_energy_mode_raw)
61+
)
62+
grid_voltage = float(grid_voltage_raw)
63+
except (TypeError, ValueError) as error:
64+
print(
65+
"EnergyUseMode or GridVoltage contains "
66+
f"an invalid value: {error}"
67+
)
68+
return False
69+
70+
is_grid_available = (
71+
grid_voltage >= GRID_OUTAGE_VOLTAGE_THRESHOLD
72+
)
73+
74+
target_mode: EnergyMode | None = None
75+
switch_reason: str | None = None
76+
77+
# Grid outage rule has the highest priority.
78+
if (
79+
ENABLE_GRID_OUTAGE_AUTO_SWITCH
80+
and not is_grid_available
81+
):
82+
target_mode = GRID_OUTAGE_TARGET_MODE
83+
switch_reason = "electrical grid is unavailable"
84+
85+
# Time-based rule is checked when the grid is available.
86+
elif (
87+
ENABLE_AUTO_SWITCH
88+
and is_grid_available
89+
and is_time_reached(AUTO_SWITCH_TARGET_TIME)
90+
):
91+
target_mode = AUTO_SWITCH_TARGET_MODE
92+
switch_reason = "auto-switch target time has been reached"
93+
94+
if target_mode is None:
95+
print("No energy mode switch is currently required.")
96+
return False
97+
98+
if current_energy_mode == target_mode:
99+
print(
100+
f"Inverter is already operating in "
101+
f"{target_mode.name} mode."
102+
)
103+
return False
104+
105+
print(
106+
f"Energy mode switch required: "
107+
f"{current_energy_mode.name} -> {target_mode.name}. "
108+
f"Reason: {switch_reason}."
109+
)
110+
111+
# The real inverter command will be added here:
112+
switch_energy_mode(
113+
target_mode=target_mode,
114+
)
115+
return True

0 commit comments

Comments
 (0)