-
-
Notifications
You must be signed in to change notification settings - Fork 90
More unit tests for Octopus and HA, New installer that doesn't depend on file list #3123
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive unit tests for Octopus API and Home Assistant interface components, significantly expanding test coverage for external integrations and core infrastructure.
Key changes:
- Added 1934-line test suite for Octopus miscellaneous API methods (intelligent scheduling, saving sessions, tariffs, rates)
- Added 5 new HAInterface test modules covering state management, API calls, services, lifecycle, and websockets (total ~3000 lines)
- Added HAHistory component tests (795 lines) for history data fetching and caching
- Fixed indentation bug in ha.py where try block was incorrectly placed
- Added test helper infrastructure in test_hainterface_common.py
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 20 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/predbat/unit_test.py | Registered 13 new test functions in TEST_REGISTRY for Octopus misc and HAInterface/HAHistory components |
| apps/predbat/tests/test_octopus_misc.py | Comprehensive tests for Octopus intelligent schedule, saving sessions, tariffs, rates retrieval, and run loop |
| apps/predbat/tests/test_hainterface_websocket.py | Tests for HAInterface websocket connection, authentication, event handling, and error recovery |
| apps/predbat/tests/test_hainterface_state.py | Tests for state management (get/set/update), caching, and database mirroring |
| apps/predbat/tests/test_hainterface_service.py | Tests for service calls via websocket/loopback, set_state_external, and CONFIG_ITEMS handling |
| apps/predbat/tests/test_hainterface_lifecycle.py | Tests for initialize(), start(), is_alive(), wait_api_started(), and component lifecycle |
| apps/predbat/tests/test_hainterface_common.py | Shared mock infrastructure (MockBase, MockDatabaseManager, helper functions) for HAInterface tests |
| apps/predbat/tests/test_hainterface_api.py | Tests for API calls (GET/POST), error handling, supervisor endpoint, and get_history() |
| apps/predbat/tests/test_hahistory.py | Tests for HAHistory component (add_entity, get_history, caching, pruning, run loop) |
| apps/predbat/ha.py | Bug fix: moved HTTP request code inside try block for proper exception handling |
| .cspell/custom-dictionary-workspace.txt | Added "hahistory", "hainterface", "socketloop" to dictionary |
| # Test 2: Get from cache (should not call HAInterface again) | ||
| initial_call_count = len(mock_ha.get_history_calls) | ||
| result2 = ha_history.get_history(entity_id, days=30, tracked=True) | ||
|
|
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result2 is not used.
| # Verify cached result matches initial result | |
| if result2 is None: | |
| print("ERROR: Cached history should not be None") | |
| failed += 1 | |
| elif len(result2) != len(result) or len(result2[0]) != len(result[0]): | |
| print("ERROR: Cached history should match initial result") | |
| failed += 1 | |
| else: | |
| print("✓ Cached history matches initial result") | |
| # Verify HAInterface was not called again (cache used) |
| print("✓ Used cache instead of fetching again") | ||
|
|
||
| # Test 3: Request more days (should fetch again) | ||
| result3 = ha_history.get_history(entity_id, days=60, tracked=True) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result3 is not used.
| result3 = ha_history.get_history(entity_id, days=60, tracked=True) | |
| ha_history.get_history(entity_id, days=60, tracked=True) |
| mock_env.return_value = "supervisor_token" | ||
| mock_get.return_value = create_mock_requests_response(200, {"supervisor": "data"}) | ||
|
|
||
| result = ha_interface.api_call("/addons/self/info", core=False) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| mock_get.return_value = mock_response | ||
|
|
||
| # Call with silent=True | ||
| result = ha_interface.api_call("/api/states", silent=True) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| with patch("ha.requests.get") as mock_get: | ||
| mock_get.side_effect = requests.Timeout("Connection timeout") | ||
|
|
||
| result = ha_interface.api_call("/api/states") |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
|
|
||
| api3.tariffs = {"import": {"data": tariff_data_before.copy()}} # Copy so we can check modification | ||
|
|
||
| result = api3.get_octopus_rates_direct("import", standingCharge=False) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to 'result' is unnecessary as it is redefined before this value is used.
| result = api3.get_octopus_rates_direct("import", standingCharge=False) | |
| api3.get_octopus_rates_direct("import", standingCharge=False) |
| # Mock datetime to be at 30-minute mark (e.g., 10:30) | ||
| with patch("octopus.datetime") as mock_datetime: | ||
| mock_datetime.now.return_value = datetime(2025, 1, 1, 10, 30, 0) | ||
| result = await api2.run(seconds=0, first=False) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to 'result' is unnecessary as it is redefined before this value is used.
| result = await api2.run(seconds=0, first=False) | |
| await api2.run(seconds=0, first=False) |
| # Mock datetime to be at 10-minute mark (e.g., 10:10) | ||
| with patch("octopus.datetime") as mock_datetime: | ||
| mock_datetime.now.return_value = datetime(2025, 1, 1, 10, 10, 0) | ||
| result = await api3.run(seconds=0, first=False) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to 'result' is unnecessary as it is redefined before this value is used.
| result = await api3.run(seconds=0, first=False) | |
| await api3.run(seconds=0, first=False) |
| # Mock datetime to be at 2-minute mark (e.g., 10:02) | ||
| with patch("octopus.datetime") as mock_datetime: | ||
| mock_datetime.now.return_value = datetime(2025, 1, 1, 10, 2, 0) | ||
| result = await api4.run(seconds=0, first=False) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to 'result' is unnecessary as it is redefined before this value is used.
| result = await api4.run(seconds=0, first=False) | |
| await api4.run(seconds=0, first=False) |
| # Mock datetime to be at non-10/30-minute mark (e.g., 10:05) | ||
| with patch("octopus.datetime") as mock_datetime: | ||
| mock_datetime.now.return_value = datetime(2025, 1, 1, 10, 5, 0) | ||
| result = await api5.run(seconds=0, first=False) |
Copilot
AI
Dec 25, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to 'result' is unnecessary as it is redefined before this value is used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated 11 comments.
| if len(mock_ha.get_history_calls) != initial_call_count: | ||
| print("ERROR: Should use cache, not call HAInterface again") | ||
| failed += 1 | ||
| else: | ||
| print("✓ Used cache instead of fetching again") |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result2 is not used.
| if len(mock_ha.get_history_calls) != initial_call_count: | |
| print("ERROR: Should use cache, not call HAInterface again") | |
| failed += 1 | |
| else: | |
| print("✓ Used cache instead of fetching again") | |
| # Verify cached result matches the original result | |
| if result2 != result: | |
| print("ERROR: Cached history does not match initial result") | |
| failed += 1 | |
| elif len(mock_ha.get_history_calls) != initial_call_count: | |
| print("ERROR: Should use cache, not call HAInterface again") | |
| failed += 1 | |
| else: | |
| print("✓ Used cache instead of fetching again and returned same data") |
| mock_get.return_value = mock_response | ||
|
|
||
| # Call with silent=True | ||
| result = ha_interface.api_call("/api/states", silent=True) |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| result = ha_interface.api_call("/api/states", silent=True) | |
| ha_interface.api_call("/api/states", silent=True) |
| with patch("ha.requests.get") as mock_get: | ||
| mock_get.side_effect = requests.Timeout("Connection timeout") | ||
|
|
||
| result = ha_interface.api_call("/api/states") |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| result = ha_interface.api_call("/api/states") | |
| ha_interface.api_call("/api/states") |
| with patch("ha.requests.get") as mock_get: | ||
| mock_get.return_value = create_mock_requests_response(200, {"result": "success"}) | ||
|
|
||
| result = ha_interface.api_call("/api/states") |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| result = ha_interface.api_call("/api/states") | |
| ha_interface.api_call("/api/states") |
|
|
||
| ha_interface.call_service_websocket_command = mock_call_service_websocket_command | ||
|
|
||
| result = ha_interface.call_service("switch/turn_on", entity_id="switch.test") |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| mock_session.ws_connect.return_value.__aexit__ = AsyncMock() | ||
| mock_session_class.return_value = mock_session | ||
|
|
||
| result = run_async(ha_interface.async_call_service_websocket_command("switch", "turn_on", {"entity_id": "switch.test", "return_response": True})) |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| mock_session.ws_connect = MagicMock(side_effect=Exception("Connection failed")) | ||
| mock_session_class.return_value = mock_session | ||
|
|
||
| result = run_async(ha_interface.async_call_service_websocket_command("switch", "turn_on", {"entity_id": "switch.test"})) |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| result = run_async(ha_interface.async_call_service_websocket_command("switch", "turn_on", {"entity_id": "switch.test"})) | |
| run_async(ha_interface.async_call_service_websocket_command("switch", "turn_on", {"entity_id": "switch.test"})) |
| mock_session.ws_connect = MagicMock(side_effect=Exception("Connection failed")) | ||
| mock_session_class.return_value = mock_session | ||
|
|
||
| result = run_async(ha_interface.async_call_service_websocket_command("switch", "turn_on", {"entity_id": "switch.test"})) |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Variable result is not used.
| result = run_async(ha_interface.async_call_service_websocket_command("switch", "turn_on", {"entity_id": "switch.test"})) | |
| run_async(ha_interface.async_call_service_websocket_command("switch", "turn_on", {"entity_id": "switch.test"})) |
| def get_history_db(self, sensor, now, days=30): | ||
| """Mock get_history_db - returns empty history""" | ||
| self.get_history_calls.append({"sensor": sensor, "now": now, "days": days}) | ||
| # Return mock history format: [[{entry1}, {entry2}, ...]] | ||
| return [[]] | ||
|
|
||
| def get_all_entities_db(self): | ||
| """Mock get_all_entities_db - returns list of entity IDs""" | ||
| return list(self.state_data.keys()) | ||
|
|
||
| def get_history_db(self, sensor, now, days=30): | ||
| """Mock get_history_db - returns empty list""" |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assignment to 'get_history_db' is unnecessary as it is redefined before this value is used.
| def get_history_db(self, sensor, now, days=30): | |
| """Mock get_history_db - returns empty history""" | |
| self.get_history_calls.append({"sensor": sensor, "now": now, "days": days}) | |
| # Return mock history format: [[{entry1}, {entry2}, ...]] | |
| return [[]] | |
| def get_all_entities_db(self): | |
| """Mock get_all_entities_db - returns list of entity IDs""" | |
| return list(self.state_data.keys()) | |
| def get_history_db(self, sensor, now, days=30): | |
| """Mock get_history_db - returns empty list""" | |
| def get_all_entities_db(self): | |
| """Mock get_all_entities_db - returns list of entity IDs""" | |
| return list(self.state_data.keys()) | |
| def get_history_db(self, sensor, now, days=30): | |
| """Mock get_history_db - returns empty list""" | |
| self.get_history_calls.append({"sensor": sensor, "now": now, "days": days}) |
| predbat_update_move(THIS_VERSION, files) | ||
| sys.exit(1) | ||
| elif modified: | ||
| print("Warn: Predbat files are installed but have modifications") |
Copilot
AI
Dec 26, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Print statement may execute during import.
This pull request adds comprehensive unit test coverage for Octopus and HAInterface components, and introduces a new installer that dynamically fetches file lists from GitHub instead of depending on a hardcoded file list. Key improvements include: