Skip to content

Commit b834082

Browse files
MaStrCopilot
andauthored
Fix: Type error for interval_minutes (#305)
* Transport time resolution as a proper parameter * Fix: PEP8 spacing in logic factory + tests for string time_resolution_minutes (#306) * Initial plan * Fix PEP8 spacing in logic.py and add string time_resolution tests Co-authored-by: MaStr <1036501+MaStr@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MaStr <1036501+MaStr@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: MaStr <1036501+MaStr@users.noreply.github.com>
1 parent b304e6f commit b834082

3 files changed

Lines changed: 74 additions & 4 deletions

File tree

src/batcontrol/core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,9 @@ def run(self):
497497
1 - elapsed_in_current
498498
)
499499

500-
this_logic_run = LogicFactory.create_logic(self.config, self.timezone)
500+
this_logic_run = LogicFactory.create_logic(self.time_resolution,
501+
self.config,
502+
self.timezone)
501503

502504
# Create input for calculation
503505
calc_input = CalculationInput(

src/batcontrol/logic/logic.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ class Logic:
1010
""" Factory for logic classes. """
1111
print_class_message = True
1212
@staticmethod
13-
def create_logic(config: dict, timezone) -> LogicInterface:
13+
def create_logic(time_resolution_minutes: int, config: dict, timezone) -> LogicInterface:
1414
""" Select and configure a logic class based on the given configuration """
1515
request_type = config.get('type', 'default').lower()
16-
interval_minutes = config.get('time_resolution_minutes', 60)
1716
logic = None
1817
if request_type == 'default':
1918
if Logic.print_class_message:
2019
logger.info('Using default logic')
2120
Logic.print_class_message = False
22-
logic = DefaultLogic(timezone, interval_minutes=interval_minutes)
21+
logic = DefaultLogic(timezone, interval_minutes=time_resolution_minutes)
2322
if config.get('battery_control_expert', None) is not None:
2423
battery_control_expert = config.get( 'battery_control_expert', {})
2524
attribute_list = [

tests/batcontrol/test_core.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Tests for core batcontrol functionality including MODE_LIMIT_BATTERY_CHARGE_RATE"""
2+
import datetime
23
import pytest
34
from unittest.mock import MagicMock, patch
45

56
from batcontrol.core import (
67
Batcontrol,
78
MODE_LIMIT_BATTERY_CHARGE_RATE,
89
)
10+
from batcontrol.logic.logic import Logic as LogicFactory
911

1012

1113
class TestModeLimitBatteryChargeRate:
@@ -287,5 +289,72 @@ def test_api_set_limit_applies_immediately_in_mode_8(
287289
mock_inverter.set_mode_limit_battery_charge.assert_called_once_with(2000)
288290

289291

292+
class TestTimeResolutionString:
293+
"""Test that time_resolution_minutes provided as string (e.g. from Home Assistant) is handled correctly"""
294+
295+
@pytest.fixture
296+
def base_mock_config(self):
297+
"""Provide a minimal config for testing"""
298+
return {
299+
'timezone': 'Europe/Berlin',
300+
'inverter': {
301+
'type': 'dummy',
302+
'max_grid_charge_rate': 5000,
303+
'max_pv_charge_rate': 3000,
304+
'min_pv_charge_rate': 100
305+
},
306+
'utility': {
307+
'type': 'tibber',
308+
'token': 'test_token'
309+
},
310+
'pvinstallations': [],
311+
'consumption_forecast': {
312+
'type': 'simple',
313+
'value': 500
314+
},
315+
'battery_control': {
316+
'max_charging_from_grid_limit': 0.8,
317+
'min_price_difference': 0.05
318+
},
319+
'mqtt': {
320+
'enabled': False
321+
}
322+
}
323+
324+
@pytest.mark.parametrize('resolution_str,expected_int', [('60', 60), ('15', 15)])
325+
@patch('batcontrol.core.tariff_factory.create_tarif_provider')
326+
@patch('batcontrol.core.inverter_factory.create_inverter')
327+
@patch('batcontrol.core.solar_factory.create_solar_provider')
328+
@patch('batcontrol.core.consumption_factory.create_consumption')
329+
def test_string_time_resolution_initialises_without_error(
330+
self, mock_consumption, mock_solar, mock_inverter_factory, mock_tariff,
331+
base_mock_config, resolution_str, expected_int
332+
):
333+
"""Batcontrol must not crash when time_resolution_minutes is a string"""
334+
mock_inverter = MagicMock()
335+
mock_inverter.get_max_capacity = MagicMock(return_value=10000)
336+
mock_inverter_factory.return_value = mock_inverter
337+
mock_tariff.return_value = MagicMock()
338+
mock_solar.return_value = MagicMock()
339+
mock_consumption.return_value = MagicMock()
340+
341+
base_mock_config['time_resolution_minutes'] = resolution_str
342+
bc = Batcontrol(base_mock_config)
343+
344+
assert isinstance(bc.time_resolution, int)
345+
assert bc.time_resolution == expected_int
346+
347+
@pytest.mark.parametrize('resolution_str', ['60', '15'])
348+
def test_logic_factory_accepts_string_resolution_as_int(self, resolution_str):
349+
"""Logic factory must produce a valid logic instance when given an int resolution"""
350+
logic = LogicFactory.create_logic(
351+
int(resolution_str),
352+
{'type': 'default'},
353+
datetime.timezone.utc
354+
)
355+
assert logic is not None
356+
assert logic.interval_minutes == int(resolution_str)
357+
358+
290359
if __name__ == '__main__':
291360
pytest.main([__file__, '-v'])

0 commit comments

Comments
 (0)