Skip to content

Commit 598a440

Browse files
committed
test: add energy mode control tests and CI workflow
1 parent b380856 commit 598a440

13 files changed

Lines changed: 312 additions & 33 deletions

File tree

.github/workflows/tests.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
tests:
12+
runs-on: ubuntu-24.04
13+
timeout-minutes: 10
14+
15+
steps:
16+
- name: Check out repository
17+
uses: actions/checkout@v6
18+
19+
- name: Set up Python
20+
uses: actions/setup-python@v6
21+
with:
22+
python-version: "3.12"
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
python -m pip install -r requirements-dev.txt
28+
29+
- name: Run tests
30+
run: python -m pytest

energy_mode_control/energy_mode_controller.py

Lines changed: 46 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Standard Libraries
44
import logging
5-
from typing import Any, Final
5+
from typing import Any, Final, Protocol
66

77
# Custom Modules
88
from config import (
@@ -13,10 +13,8 @@
1313
GRID_OUTAGE_TARGET_MODE,
1414
EnergyMode
1515
)
16+
from energy_mode_control.mode_decision import decide_target_mode
1617
from energy_mode_control.time_utils import is_time_reached
17-
from energy_mode_control.energy_mode_switcher import (
18-
switch_energy_mode,
19-
)
2018

2119

2220
LOGGER = logging.getLogger(__name__)
@@ -26,15 +24,26 @@
2624
GRID_OUTAGE_VOLTAGE_THRESHOLD: Final[float] = 10.0
2725

2826

29-
def handle_energy_mode_control(must_data: dict[str, Any] | None) -> bool:
27+
class InverterClient(Protocol):
28+
def switch_energy_mode(self, target_mode: EnergyMode) -> None:
29+
"""Switch the inverter to the requested energy mode."""
30+
31+
32+
def handle_energy_mode_control(
33+
must_data: dict[str, Any] | None,
34+
inverter_client: InverterClient,
35+
) -> bool:
3036
try:
31-
return _handle_energy_mode_control(must_data)
37+
return _handle_energy_mode_control(must_data, inverter_client)
3238
except Exception:
3339
LOGGER.exception("Failed to handle energy mode control.")
3440
return False
3541

3642

37-
def _handle_energy_mode_control(must_data: dict[str, Any] | None) -> bool:
43+
def _handle_energy_mode_control(
44+
must_data: dict[str, Any] | None,
45+
inverter_client: InverterClient,
46+
) -> bool:
3847
if not ENABLE_AUTO_SWITCH and not ENABLE_GRID_OUTAGE_AUTO_SWITCH:
3948
LOGGER.debug("All automatic energy mode control features are disabled.")
4049
return False
@@ -76,46 +85,51 @@ def _handle_energy_mode_control(must_data: dict[str, Any] | None) -> bool:
7685
grid_voltage >= GRID_OUTAGE_VOLTAGE_THRESHOLD
7786
)
7887

79-
target_mode: EnergyMode | None = None
80-
switch_reason: str | None = None
81-
82-
# Grid outage rule has the highest priority.
83-
if (
84-
ENABLE_GRID_OUTAGE_AUTO_SWITCH
85-
and not is_grid_available
86-
):
87-
target_mode = GRID_OUTAGE_TARGET_MODE
88-
switch_reason = "electrical grid is unavailable"
89-
90-
# Time-based rule is checked when the grid is available.
91-
elif (
88+
is_auto_switch_time_reached = (
9289
ENABLE_AUTO_SWITCH
9390
and is_grid_available
9491
and is_time_reached(AUTO_SWITCH_TARGET_TIME)
95-
):
96-
target_mode = AUTO_SWITCH_TARGET_MODE
97-
switch_reason = "auto-switch target time has been reached"
92+
)
93+
target_mode = decide_target_mode(
94+
is_grid_available=is_grid_available,
95+
is_auto_switch_enabled=ENABLE_AUTO_SWITCH,
96+
is_auto_switch_time_reached=is_auto_switch_time_reached,
97+
auto_switch_target_mode=AUTO_SWITCH_TARGET_MODE,
98+
is_grid_outage_auto_switch_enabled=(
99+
ENABLE_GRID_OUTAGE_AUTO_SWITCH
100+
),
101+
grid_outage_target_mode=GRID_OUTAGE_TARGET_MODE,
102+
)
103+
104+
return execute_mode_decision(
105+
current_mode=current_energy_mode,
106+
target_mode=target_mode,
107+
inverter_client=inverter_client,
108+
)
109+
110+
111+
def execute_mode_decision(
112+
current_mode: EnergyMode,
113+
target_mode: EnergyMode | None,
114+
inverter_client: InverterClient,
115+
) -> bool:
116+
"""Execute a mode decision when the target differs from current mode."""
98117

99118
if target_mode is None:
100119
LOGGER.debug("No energy mode switch is currently required.")
101120
return False
102121

103-
if current_energy_mode == target_mode:
122+
if current_mode == target_mode:
104123
LOGGER.debug(
105124
"Inverter is already operating in %s mode.",
106125
target_mode.name,
107126
)
108127
return False
109128

110129
LOGGER.info(
111-
"Energy mode switch required: %s -> %s. Reason: %s.",
112-
current_energy_mode.name,
130+
"Energy mode switch required: %s -> %s.",
131+
current_mode.name,
113132
target_mode.name,
114-
switch_reason,
115-
)
116-
117-
# The real inverter command will be added here:
118-
switch_energy_mode(
119-
target_mode=target_mode,
120133
)
134+
inverter_client.switch_energy_mode(target_mode=target_mode)
121135
return True
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# energy_mode_control/mode_decision.py
2+
3+
# Custom Modules
4+
from config import EnergyMode
5+
6+
7+
def decide_target_mode(
8+
*,
9+
is_grid_available: bool,
10+
is_auto_switch_enabled: bool,
11+
is_auto_switch_time_reached: bool,
12+
auto_switch_target_mode: EnergyMode,
13+
is_grid_outage_auto_switch_enabled: bool,
14+
grid_outage_target_mode: EnergyMode,
15+
) -> EnergyMode | None:
16+
"""Return the required target mode without performing side effects."""
17+
if is_grid_outage_auto_switch_enabled and not is_grid_available:
18+
return grid_outage_target_mode
19+
20+
if (
21+
is_auto_switch_enabled
22+
and is_grid_available
23+
and is_auto_switch_time_reached
24+
):
25+
return auto_switch_target_mode
26+
27+
return None

energy_mode_control/time_utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ def get_current_kyiv_datetime() -> datetime:
2020
return datetime.now(tz=KYIV_TIMEZONE)
2121

2222

23+
def is_time_in_day_window(
24+
current_time: time,
25+
start_time: time,
26+
end_time: time,
27+
) -> bool:
28+
"""Return whether the current time is in a daytime interval."""
29+
return start_time <= current_time < end_time
30+
31+
2332
def is_time_reached(
2433
target_time: time,
2534
current_datetime: datetime | None = None,

main.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
MYSQL_HOST,
2525
MYSQL_PASSWORD,
2626
MYSQL_USER,
27+
EnergyMode,
2728
get_startup_configuration_summary,
2829
log_configuration_warnings,
2930
)
@@ -149,6 +150,14 @@ def get_data(self) -> Optional[dict]:
149150
except Exception:
150151
LOGGER.exception("Failed to get inverter data.")
151152

153+
def switch_energy_mode(self, target_mode: EnergyMode) -> None:
154+
from energy_mode_control.energy_mode_switcher import switch_energy_mode
155+
156+
switch_energy_mode(
157+
target_mode=target_mode,
158+
port=self._serial_port,
159+
)
160+
152161

153162
def main():
154163
configure_logging()
@@ -198,7 +207,10 @@ def main():
198207
LOGGER.info("Inverter data inserted into the database.")
199208

200209
# 3. Handle optional energy mode control
201-
handle_energy_mode_control(must_data=must_data)
210+
handle_energy_mode_control(
211+
must_data=must_data,
212+
inverter_client=must_inverter_data_handler,
213+
)
202214
else:
203215
LOGGER.warning("Unable to get data from the inverter.")
204216

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[pytest]
2+
testpaths = tests
3+
addopts = -ra

requirements-dev.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-r requirements.txt
2+
pytest~=9.1

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# tests/__init__.py

tests/conftest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# tests/conftest.py
2+
3+
# Third-party Libraries
4+
import dotenv
5+
6+
7+
# Keep business-logic tests independent from the developer's local .env file.
8+
dotenv.load_dotenv = lambda *args, **kwargs: False
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# tests/energy_mode_control/__init__.py

0 commit comments

Comments
 (0)