Skip to content

Conversation

@springfall2008
Copy link
Owner

@springfall2008 springfall2008 commented Dec 25, 2025

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:

  • Addition of 1900+ lines of unit tests for Octopus misc API methods (set_intelligent_target_schedule, join_saving_session_events, fetch_tariffs, get_octopus_rates_direct, etc.)
  • New HAInterface test suite split into logical modules (state, service, API, lifecycle, websocket)
  • HAHistory component tests covering caching, pruning, and incremental updates
  • Download/installer tests validating GitHub API integration and manifest-based verification
  • Refactored installer using GitHub API directory listing + YAML manifest for dynamic file discovery

Copilot AI review requested due to automatic review settings December 25, 2025 21:31
Copy link
Contributor

Copilot AI left a 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)

Copy link

Copilot AI Dec 25, 2025

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.

Suggested change
# 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)

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 25, 2025

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.

Suggested change
result3 = ha_history.get_history(entity_id, days=60, tracked=True)
ha_history.get_history(entity_id, days=60, tracked=True)

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Dec 25, 2025

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.

Copilot uses AI. Check for mistakes.
mock_get.return_value = mock_response

# Call with silent=True
result = ha_interface.api_call("/api/states", silent=True)
Copy link

Copilot AI Dec 25, 2025

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.

Copilot uses AI. Check for mistakes.
with patch("ha.requests.get") as mock_get:
mock_get.side_effect = requests.Timeout("Connection timeout")

result = ha_interface.api_call("/api/states")
Copy link

Copilot AI Dec 25, 2025

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.

Copilot uses AI. Check for mistakes.

api3.tariffs = {"import": {"data": tariff_data_before.copy()}} # Copy so we can check modification

result = api3.get_octopus_rates_direct("import", standingCharge=False)
Copy link

Copilot AI Dec 25, 2025

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.

Suggested change
result = api3.get_octopus_rates_direct("import", standingCharge=False)
api3.get_octopus_rates_direct("import", standingCharge=False)

Copilot uses AI. Check for mistakes.
# 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)
Copy link

Copilot AI Dec 25, 2025

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.

Suggested change
result = await api2.run(seconds=0, first=False)
await api2.run(seconds=0, first=False)

Copilot uses AI. Check for mistakes.
# 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)
Copy link

Copilot AI Dec 25, 2025

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.

Suggested change
result = await api3.run(seconds=0, first=False)
await api3.run(seconds=0, first=False)

Copilot uses AI. Check for mistakes.
# 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)
Copy link

Copilot AI Dec 25, 2025

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.

Suggested change
result = await api4.run(seconds=0, first=False)
await api4.run(seconds=0, first=False)

Copilot uses AI. Check for mistakes.
# 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)
Copy link

Copilot AI Dec 25, 2025

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.

Copilot uses AI. Check for mistakes.
@springfall2008 springfall2008 changed the title More unit tests for Octopus and HA More unit tests for Octopus and HA, New installer that doesn't depend on file list Dec 26, 2025
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +251 to +255
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")
Copy link

Copilot AI Dec 26, 2025

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.

Suggested change
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")

Copilot uses AI. Check for mistakes.
mock_get.return_value = mock_response

# Call with silent=True
result = ha_interface.api_call("/api/states", silent=True)
Copy link

Copilot AI Dec 26, 2025

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.

Suggested change
result = ha_interface.api_call("/api/states", silent=True)
ha_interface.api_call("/api/states", silent=True)

Copilot uses AI. Check for mistakes.
with patch("ha.requests.get") as mock_get:
mock_get.side_effect = requests.Timeout("Connection timeout")

result = ha_interface.api_call("/api/states")
Copy link

Copilot AI Dec 26, 2025

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.

Suggested change
result = ha_interface.api_call("/api/states")
ha_interface.api_call("/api/states")

Copilot uses AI. Check for mistakes.
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")
Copy link

Copilot AI Dec 26, 2025

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.

Suggested change
result = ha_interface.api_call("/api/states")
ha_interface.api_call("/api/states")

Copilot uses AI. Check for mistakes.

ha_interface.call_service_websocket_command = mock_call_service_websocket_command

result = ha_interface.call_service("switch/turn_on", entity_id="switch.test")
Copy link

Copilot AI Dec 26, 2025

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.

Copilot uses AI. Check for mistakes.
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}))
Copy link

Copilot AI Dec 26, 2025

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.

Copilot uses AI. Check for mistakes.
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"}))
Copy link

Copilot AI Dec 26, 2025

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.

Suggested change
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"}))

Copilot uses AI. Check for mistakes.
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"}))
Copy link

Copilot AI Dec 26, 2025

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.

Suggested change
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"}))

Copilot uses AI. Check for mistakes.
Comment on lines 73 to 84
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"""
Copy link

Copilot AI Dec 26, 2025

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.

Suggested change
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})

Copilot uses AI. Check for mistakes.
predbat_update_move(THIS_VERSION, files)
sys.exit(1)
elif modified:
print("Warn: Predbat files are installed but have modifications")
Copy link

Copilot AI Dec 26, 2025

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.

Copilot uses AI. Check for mistakes.
@springfall2008 springfall2008 merged commit 8ab260b into main Dec 26, 2025
1 check passed
@springfall2008 springfall2008 deleted the fixes4 branch December 26, 2025 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants