Skip to content
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

Multiple chargers with central services #1498

Open
wants to merge 72 commits into
base: main
Choose a base branch
from
Open

Conversation

drc38
Copy link
Collaborator

@drc38 drc38 commented Jan 24, 2025

This can be tested here: https://github.com/drc38/ocpp/tree/0.8.6
Please take a backup first as the config entry will be migrated and to go back will require restoring the backup or starting from a clean install of an older version of the integration.

Summary by CodeRabbit

Release Notes for OCPP Integration v0.8.0

  • New Features

    • Added support for multiple charge points in a single configuration.
    • Enhanced configuration flow with separate central system and charger settings.
    • Improved WebSocket connection management with new timeout and ping settings.
    • Introduced new service handlers for firmware updates, diagnostics retrieval, and charge rate adjustments.
  • Configuration Changes

    • Restructured configuration to allow multiple charge point configurations.
    • Added new WebSocket-related configuration options.
    • Separated central system and charger configuration steps.
  • Bug Fixes

    • Improved error handling for charge point connections.
    • Enhanced service call reliability.
    • Updated configuration migration process.
  • Performance Improvements

    • Optimized charge point connection and service management.
    • Refined metric and sensor handling for multiple charge points.
  • Breaking Changes

    • Configuration structure has been significantly updated.
    • Charge point identifier (CPID) handling modified.
    • Removed support for certain service versions in the integration.

@drc38 drc38 temporarily deployed to continuous-integration January 28, 2025 10:13 — with GitHub Actions Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tests/test_config_flow.py (2)

72-124: Add test coverage for measurand selection error cases.

While the discovery flow test is thorough, consider adding test cases for:

  1. No measurands selected
  2. Invalid measurand combinations
  3. Error recovery paths

Example test case for no measurands selected:

# Test no measurands selected
empty_measurands = {value: False for value in DEFAULT_MONITORED_VARIABLES.split(",")}
result_empty = await hass.config_entries.flow.async_configure(
    result_cp["flow_id"], user_input=empty_measurands
)
assert result_empty["type"] == data_entry_flow.FlowResultType.FORM
assert result_empty["errors"] == {"base": "no_measurands_selected"}

126-161: Add test case for non-duplicate CP IDs.

The test effectively handles duplicate CP IDs, but consider adding a test case for different CP IDs to ensure the system correctly allows multiple unique chargers.

Example addition:

# Test different CP IDs are allowed
info2 = {"cp_id": "different_cp_id", "entry": entry}
result3 = await hass.config_entries.flow.async_init(
    DOMAIN,
    context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
    data=info2,
)
assert result3["type"] == data_entry_flow.FlowResultType.FORM
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cae9a41 and 00cac7d.

📒 Files selected for processing (1)
  • tests/test_config_flow.py (2 hunks)
🧰 Additional context used
📓 Learnings (1)
tests/test_config_flow.py (1)
Learnt from: drc38
PR: lbbrhzn/ocpp#1498
File: tests/test_config_flow.py:124-124
Timestamp: 2025-01-28T09:21:24.150Z
Learning: The OCPP integration does not use options flow for configuration updates. Error handling in the config flow is primarily focused on measurand selection validation with the "no_measurands_selected" error.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run tests
🔇 Additional comments (3)
tests/test_config_flow.py (3)

5-21: LGTM! Well-organized imports and constants.

The imports and constants are properly organized and all are necessary for the test cases.


57-68: LGTM! Proper handling of configuration data.

The test correctly removes the CONF_CPIDS before flow configuration as it's added during the flow, and the assertions are properly updated.


Line range hint 1-184: LGTM! Well-structured test suite with good coverage.

The test suite is well-organized and covers the main configuration flows. The removal of options flow tests is correct as confirmed by the retrieved learnings. While the current coverage is good, the suggested improvements would make it more robust.

tests/test_config_flow.py Show resolved Hide resolved
@rbb-dev
Copy link

rbb-dev commented Jan 28, 2025

Measureands selection is working correctly now, but as soon as the second charger is connected, the first charger becomes unavailable.

2025-01-28 22:29:15.268 INFO (MainThread) [ocpp] charger_ac: send [3,"2b7887b4-f8d6-47bf-b4c5-5d14dff3772e",{}]
2025-01-28 22:29:40.562 ERROR (MainThread) [homeassistant.components.automation.charger_ac_send_configuration] Charger AC - Send Configuration: Error executing script. Service not found for call_service at pos 2: Action charger_ac.configure not found
2025-01-28 22:37:09.578 INFO (MainThread) [ocpp] charger_ac: receive message [2,"b72b3b33-d6af-4d8c-8227-561ed1cb6d2f","StatusNotification",{"connectorId":1,"errorCode":"NoError","status":"Available","timestamp":"2025-01-28T11:37:07.000Z"}]
2025-01-28 22:37:09.595 INFO (MainThread) [ocpp] charger_ac: send [3,"b72b3b33-d6af-4d8c-8227-561ed1cb6d2f",{}]
2025-01-28 22:39:02.366 ERROR (MainThread) [websockets.server] connection handler failed
Traceback (most recent call last):
  File "/usr/local/lib/python3.13/site-packages/websockets/asyncio/server.py", line 374, in conn_handler
    await self.handler(connection)
  File "/config/custom_components/ocpp/api.py", line 229, in on_connect
    self.cpids.update({cp_settings.cpid: cp_id})
                       ^^^^^^^^^^^
UnboundLocalError: cannot access local variable 'cp_settings' where it is not associated with a value
2025-01-28 22:41:03.308 INFO (MainThread) [ocpp] charger_ac: receive message [2,"656ad97f-9eb6-4ee5-8756-c7c88d943fcf","BootNotification",{"chargePointModel":"ASOCPP0101","chargePointVendor":"[email protected]","chargePointSerialNumber":"charger_ac","firmwareVersion":"V2.1.19-ThreePhase"}]
2025-01-28 22:41:03.316 INFO (MainThread) [ocpp] charger_ac: send [3,"656ad97f-9eb6-4ee5-8756-c7c88d943fcf",{"currentTime":"2025-01-28T11:41:03Z","interval":3600,"status":"Accepted"}]
2025-01-28 22:41:03.536 INFO (MainThread) [ocpp] charger_ac: send [2,"014e6844-f6f4-40df-b853-ff2c009c9477","GetConfiguration",{"key":["SupportedFeatureProfiles"]}]
2025-01-28 22:41:05.678 INFO (MainThread) [ocpp] charger_ac: receive message [2,"c79e131b-441c-4dca-9812-6da21a7cb658","MeterValues",{"connectorId":1,"meterValue":[{"timestamp":"2025-01-28T11:41:03.000Z","sampledValue":[{"value":"0.0","measurand":"Voltage","unit":"V","phase":"L1","context":"Sample.Clock"},{"value":"0.2","measurand":"Voltage","unit":"V","phase":"L2","context":"Sample.Clock"},{"value":"242.9","measurand":"Voltage","unit":"V","phase":"L3","context":"Sample.Clock"},{"value":"0.0","measurand":"Current.Import","unit":"A","phase":"L1","context":"Sample.Clock"},{"value":"0.0","measurand":"Current.Import","unit":"A","phase":"L2","context":"Sample.Clock"},{"value":"0.0","measurand":"Current.Import","unit":"A","phase":"L3","context":"Sample.Clock"},{"value":"30","measurand":"Temperature","unit":"Celsius","context":"Sample.Clock"},{"value":"0","measurand":"Power.Active.Import","unit":"W","context":"Sample.Clock"},{"value":"0","measurand":"Energy.Active.Import.Register","unit":"Wh","context":"Sample.Clock"}]}]}]
2025-01-28 22:41:05.688 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_transaction_id not found.
2025-01-28 22:41:05.688 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_power_active_import not found.
2025-01-28 22:41:05.689 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_time_session not found.
2025-01-28 22:41:05.689 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_energy_meter_start not found.
2025-01-28 22:41:05.689 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_energy_session not found.
2025-01-28 22:41:05.690 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_current_import not found.
2025-01-28 22:41:05.690 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_energy_active_import_register not found.
2025-01-28 22:41:05.690 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_temperature not found.
2025-01-28 22:41:05.691 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_voltage not found.
2025-01-28 22:41:05.691 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_status not found.
2025-01-28 22:41:05.691 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_status_connector not found.
2025-01-28 22:41:05.691 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_heartbeat not found.
2025-01-28 22:41:05.692 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_latency_ping not found.
2025-01-28 22:41:05.692 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_latency_pong not found.
2025-01-28 22:41:05.692 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_error_code not found.
2025-01-28 22:41:05.693 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_error_code_connector not found.
2025-01-28 22:41:05.693 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_stop_reason not found.
2025-01-28 22:41:05.693 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_status_firmware not found.
2025-01-28 22:41:05.694 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_reconnects not found.
2025-01-28 22:41:05.694 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_id_tag not found.
2025-01-28 22:41:05.695 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_id not found.
2025-01-28 22:41:05.695 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_model not found.
2025-01-28 22:41:05.695 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_vendor not found.
2025-01-28 22:41:05.696 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_serial not found.
2025-01-28 22:41:05.696 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_version_firmware not found.
2025-01-28 22:41:05.696 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_features not found.
2025-01-28 22:41:05.697 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_connectors not found.
2025-01-28 22:41:05.697 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_timestamp_data_response not found.
2025-01-28 22:41:05.697 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_timestamp_data_transfer not found.
2025-01-28 22:41:05.697 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity sensor.charger_ac_timestamp_config_response not found.
2025-01-28 22:41:05.698 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity switch.charger_ac_charge_control not found.
2025-01-28 22:41:05.698 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity switch.charger_ac_availability not found.
2025-01-28 22:41:05.698 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity number.charger_ac_maximum_current not found.
2025-01-28 22:41:05.699 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity button.charger_ac_reset not found.
2025-01-28 22:41:05.699 WARNING (MainThread) [homeassistant.helpers.entity_component] Forced update failed. Entity button.charger_ac_unlock not found.
2025-01-28 22:41:05.703 INFO (MainThread) [ocpp] charger_ac: send [3,"c79e131b-441c-4dca-9812-6da21a7cb658",{}]

try EV ocpp simulator, you will see what it does.
last changer connected kicks previous charger off.

@drc38
Copy link
Collaborator Author

drc38 commented Jan 28, 2025

grep -i ocpp core.
     {"created_at":"2025-01-27T07:30:11.937178+00:00","data":{"cpids":[],"csid":"central","host":"0.0.0.0","port":9053,"ssl":false,"ssl_certfile_path":"/config/fullchain.pem","ssl_keyfile_path":"/config/privkey.pem","websocket_close_timeout":10,"websocket_ping_interval":20,"websocket_ping_timeout":20,"websocket_ping_tries":2},"disabled_by":null,"discovery_keys":{},"domain":"ocpp","entry_id":"01JJKANZV1GQQ7E3FA8WZCQ2HV","minor_version":0,"modified_at":"2025-01-27T07:30:11.937194+00:00","options":{},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","title":"central","unique_id":null,"version":2}

Thanks, @rbb-dev can you post this again for the latest failure please.

@drc38 drc38 temporarily deployed to continuous-integration January 29, 2025 07:24 — with GitHub Actions Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
tests/conftest.py (1)

44-46: Consider parameterizing the charge point ID mock.

Given that this PR adds support for multiple chargers, the hard-coded "test_cp_id" return value might not be sufficient for testing all scenarios. Consider:

  1. Parameterizing the fixture to accept different charge point IDs
  2. Adding test cases for multiple charger scenarios

Here's a suggested implementation:

 @pytest.fixture(name="bypass_get_data")
-def bypass_get_data_fixture():
+def bypass_get_data_fixture(request):
     """Skip calls to get data from API."""
     future = asyncio.Future()
     future.set_result(websockets.asyncio.server.Server)
+    cp_id = getattr(request, "param", "test_cp_id")
     with (
         patch("websockets.asyncio.server.serve", return_value=future),
         patch("websockets.asyncio.server.Server.close"),
         patch("websockets.asyncio.server.Server.wait_closed"),
-        patch("homeassistant.core.StateMachine.get", return_value="test_cp_id"),
+        patch("homeassistant.core.StateMachine.get", return_value=cp_id),
     ):
         yield

Usage example:

@pytest.mark.parametrize("bypass_get_data", ["cp_1", "cp_2"], indirect=True)
def test_multiple_chargers(bypass_get_data):
    # Test implementation
    pass
custom_components/ocpp/__init__.py (1)

18-58: Consider grouping related configuration constants.

The configuration constants could be better organized by grouping them into related categories:

  • Network configuration (HOST, PORT, SSL)
  • WebSocket settings (WEBSOCKET_*)
  • Charger settings (IDLE_INTERVAL, MAX_CURRENT)
  • Monitoring settings (MONITORED_VARIABLES)
 # Network settings
 CONF_HOST = ...
 CONF_PORT = ...
 CONF_SSL = ...
 CONF_SSL_CERTFILE_PATH = ...
 CONF_SSL_KEYFILE_PATH = ...
 
 # WebSocket settings
 CONF_WEBSOCKET_CLOSE_TIMEOUT = ...
 CONF_WEBSOCKET_PING_TRIES = ...
 CONF_WEBSOCKET_PING_INTERVAL = ...
 CONF_WEBSOCKET_PING_TIMEOUT = ...
 
 # Charger settings
 CONF_IDLE_INTERVAL = ...
 CONF_MAX_CURRENT = ...
 CONF_METER_INTERVAL = ...
 
 # Monitoring settings
 CONF_MONITORED_VARIABLES = ...
 CONF_MONITORED_VARIABLES_AUTOCONFIG = ...
tests/test_config_flow.py (4)

55-67: Enhance basic config flow test coverage.

Add assertions to verify:

  1. The structure of the created entry
  2. The presence of required fields
  3. The correct handling of default values
     assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
     assert result["title"] == "test_csid_flow"
     assert result["data"] == MOCK_CONFIG_CS
     assert result["result"]
+    # Verify entry structure
+    assert CONF_HOST in result["data"]
+    assert CONF_PORT in result["data"]
+    # Verify default values
+    assert result["data"].get(CONF_SSL) == DEFAULT_SSL
+    assert result["data"].get(CONF_WEBSOCKET_PING_TRIES) == DEFAULT_WEBSOCKET_PING_TRIES

70-143: Add error case handling and improve test documentation.

The discovery flow test should include:

  1. Documentation of test scenarios
  2. Error case handling for measurand configuration
  3. Verification of state changes
 async def test_successful_discovery_flow(hass, bypass_get_data):
-    """Test a discovery config flow."""
+    """Test a discovery config flow.
+    
+    Test cases:
+    1. Initial charger discovery and configuration
+    2. Manual measurand selection
+    3. Addition of second charger
+    """
     # Mock the config entry for the central system
     config_entry = MockConfigEntry(
         domain=DOMAIN,
         data=MOCK_CONFIG_CS,
         entry_id="test_cms_disc",
         title="test_cms_disc",
         version=2,
         minor_version=0,
     )
+    
+    # Test error cases for measurand configuration
+    with pytest.raises(InvalidData):
+        measurand_input = {}  # Empty measurands should fail
+        await hass.config_entries.flow.async_configure(
+            result_cp["flow_id"], user_input=measurand_input
+        )

145-181: Add edge cases to duplicate CPID test.

Consider adding test cases for:

  1. Case-sensitive CP ID handling
  2. Special character handling in CP IDs
  3. Retry behavior after abort
 async def test_duplicate_cpid_discovery_flow(hass, bypass_get_data):
     """Test discovery flow with duplicate CP ID."""
+    # Test case sensitivity
+    info_case = {"cp_id": "TEST_CP_ID", "entry": entry}
+    result_case = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
+        data=info_case,
+    )
+    assert result_case["type"] == data_entry_flow.FlowResultType.FORM
+    
+    # Test special characters
+    info_special = {"cp_id": "test-cp@id", "entry": entry}
+    result_special = await hass.config_entries.flow.async_init(
+        DOMAIN,
+        context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
+        data=info_special,
+    )
+    assert result_special["type"] == data_entry_flow.FlowResultType.FORM

183-202: Expand failed config flow test scenarios.

Add test cases for:

  1. Invalid port numbers
  2. Invalid SSL configurations
  3. Network timeout scenarios
  4. Invalid websocket parameters
 async def test_failed_config_flow(hass, error_on_get_data):
     """Test failed config flow scenarios."""
+    # Test invalid port
+    invalid_port_config = MOCK_CONFIG_CS.copy()
+    invalid_port_config[CONF_PORT] = -1
+    with pytest.raises(InvalidData):
+        await hass.config_entries.flow.async_configure(
+            result["flow_id"], user_input=invalid_port_config
+        )
+
+    # Test invalid SSL config
+    invalid_ssl_config = MOCK_CONFIG_CS.copy()
+    invalid_ssl_config[CONF_SSL] = True
+    invalid_ssl_config[CONF_SSL_CERTFILE_PATH] = "nonexistent.pem"
+    with pytest.raises(InvalidData):
+        await hass.config_entries.flow.async_configure(
+            result["flow_id"], user_input=invalid_ssl_config
+        )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00cac7d and 803f8d9.

📒 Files selected for processing (3)
  • custom_components/ocpp/__init__.py (2 hunks)
  • tests/conftest.py (2 hunks)
  • tests/test_config_flow.py (2 hunks)
🧰 Additional context used
📓 Learnings (1)
tests/test_config_flow.py (1)
Learnt from: drc38
PR: lbbrhzn/ocpp#1498
File: tests/test_config_flow.py:124-124
Timestamp: 2025-01-28T09:21:24.150Z
Learning: The OCPP integration does not use options flow for configuration updates. Error handling in the config flow is primarily focused on measurand selection validation with the "no_measurands_selected" error.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run tests
🔇 Additional comments (2)
tests/conftest.py (2)

34-34: Well-documented patch purpose!

The comment clearly explains that the state machine patch is used for migration purposes to return the charge point ID.


Line range hint 35-43: Well-structured fixture for API mocking!

The fixture effectively mocks the websockets server behavior. Consider adding test cases that verify the behavior when the websocket connection fails or times out.

Let's check if there are tests covering websocket failure scenarios:

Comment on lines +191 to +206
# Close server
central_sys = hass.data[DOMAIN][entry.entry_id]
central_sys._server.close()
await central_sys._server.wait_closed()
unloaded = await hass.config_entries.async_unload_platforms(
entry, PLATFORMS
)
# Unload services
# print(hass.services.async_services_for_domain(DOMAIN))
for service in hass.services.async_services_for_domain(DOMAIN):
hass.services.async_remove(DOMAIN, service)
# Unload platforms if a charger connected
if central_sys.connections == 0:
unloaded = True
else:
unloaded = await hass.config_entries.async_unload_platforms(
entry, PLATFORMS
)
# Remove entry
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add timeout and error handling to unload process.

The unload entry function could be improved with:

  1. Timeout for server closure to prevent hanging
  2. Error handling for service removal
  3. Clear documentation of connection state handling
 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Handle removal of an entry."""
     unloaded = False
     if DOMAIN in hass.data:
         if entry.entry_id in hass.data[DOMAIN]:
             central_sys = hass.data[DOMAIN][entry.entry_id]
+            try:
+                # Add timeout to prevent hanging
+                with async_timeout.timeout(10):
                     central_sys._server.close()
                     await central_sys._server.wait_closed()
+            except asyncio.TimeoutError:
+                _LOGGER.error("Timeout waiting for server to close")
+            except Exception as err:
+                _LOGGER.error("Error closing server: %s", err)
 
+            # Document connection state handling
+            _LOGGER.debug("Current connections: %d", central_sys.connections)
             for service in hass.services.async_services_for_domain(DOMAIN):
-                hass.services.async_remove(DOMAIN, service)
+                try:
+                    hass.services.async_remove(DOMAIN, service)
+                except Exception as err:
+                    _LOGGER.error("Error removing service %s: %s", service, err)

Committable suggestion skipped: line range outside the PR's diff.

custom_components/ocpp/__init__.py Show resolved Hide resolved
* fix on connect issues

* fix entity state test
@drc38 drc38 temporarily deployed to continuous-integration January 30, 2025 00:56 — with GitHub Actions Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (1)
custom_components/ocpp/api.py (1)

Line range hint 150-161: Enhance SSL context security configuration.

The SSL context creation lacks important security configurations.

Add these security enhancements:

 if self.settings.ssl:
     self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
+    self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
+    self.ssl_context.verify_mode = ssl.CERT_REQUIRED
+    self.ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
     localhost_certfile = self.settings.certfile
     localhost_keyfile = self.settings.keyfile
🧹 Nitpick comments (4)
custom_components/ocpp/number.py (1)

Line range hint 194-198: Improve native value handling.

The current implementation updates the native value even when the retrieved value is None, which could lead to inconsistent state.

Consider this improvement:

-value = self.central_system.get_metric(self.cpid, self.metric)
-if value is not None:
-    self._attr_native_value = value
-return self._attr_native_value
+new_value = self.central_system.get_metric(self.cpid, self.metric)
+if new_value is not None:
+    self._attr_native_value = new_value
+    self.async_write_ha_state()
+return new_value
custom_components/ocpp/switch.py (1)

Line range hint 115-125: Simplify state handling in is_on property.

The current implementation has nested conditions and multiple state updates. This could be simplified for better readability.

Consider this improvement:

-if self.entity_description.metric_state is not None:
-    resp = self.central_system.get_metric(
-        self.cpid, self.entity_description.metric_state
-    )
-    if resp in self.entity_description.metric_condition:
-        self._state = True
-    else:
-        self._state = False
-return self._state
+if self.entity_description.metric_state is None:
+    return self._state
+
+resp = self.central_system.get_metric(
+    self.cpid, self.entity_description.metric_state
+)
+self._state = resp in self.entity_description.metric_condition
+return self._state
custom_components/ocpp/sensor.py (2)

44-82: Optimize sensor entity creation.

The current implementation creates a new list for each charger and processes metrics individually. This could be optimized.

Consider using a set for unique metrics and list comprehension:

-SENSORS = []
-for metric in list(
-    set(
-        cp_id_settings[CONF_MONITORED_VARIABLES].split(",")
-        + list(HAChargerSession)
-    )
-):
-    SENSORS.append(
-        OcppSensorDescription(
-            key=metric.lower(),
-            name=metric.replace(".", " "),
-            metric=metric,
-        )
-    )
+monitored_metrics = set(cp_id_settings[CONF_MONITORED_VARIABLES].split(","))
+session_metrics = set(HAChargerSession)
+SENSORS = [
+    OcppSensorDescription(
+        key=metric.lower(),
+        name=metric.replace(".", " "),
+        metric=metric,
+    )
+    for metric in monitored_metrics | session_metrics
+]

Line range hint 213-220: Improve state restoration robustness.

The state restoration doesn't validate the restored values against current configuration.

Add validation for restored values:

 if restored := await self.async_get_last_sensor_data():
+    if restored.native_unit_of_measurement == self._attr_native_unit_of_measurement:
         self._attr_native_value = restored.native_value
-        self._attr_native_unit_of_measurement = restored.native_unit_of_measurement
+    else:
+        _LOGGER.warning(
+            "Restored unit %s doesn't match current unit %s for %s",
+            restored.native_unit_of_measurement,
+            self._attr_native_unit_of_measurement,
+            self.entity_id,
+        )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 803f8d9 and ca427dc.

📒 Files selected for processing (7)
  • custom_components/ocpp/api.py (5 hunks)
  • custom_components/ocpp/button.py (3 hunks)
  • custom_components/ocpp/number.py (4 hunks)
  • custom_components/ocpp/sensor.py (5 hunks)
  • custom_components/ocpp/switch.py (5 hunks)
  • custom_components/ocpp/translations/en.json (3 hunks)
  • custom_components/ocpp/translations/i-default.json (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • custom_components/ocpp/translations/en.json
  • custom_components/ocpp/translations/i-default.json
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run tests
🔇 Additional comments (1)
custom_components/ocpp/button.py (1)

92-93: Add error handling for invalid charger configurations.

The code assumes that the charger settings will always be valid. Consider adding validation and error handling.

Also applies to: 96-97

custom_components/ocpp/button.py Outdated Show resolved Hide resolved
custom_components/ocpp/number.py Show resolved Hide resolved
custom_components/ocpp/api.py Outdated Show resolved Hide resolved
custom_components/ocpp/api.py Outdated Show resolved Hide resolved
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@drc38 drc38 temporarily deployed to continuous-integration January 30, 2025 01:09 — with GitHub Actions Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
custom_components/ocpp/api.py (1)

361-474: 🛠️ Refactor suggestion

Reduce code duplication in service handlers.

The error handling code is duplicated across all service handlers. As suggested in a previous review, consider using a decorator.

This issue was previously identified. Please refer to the existing suggestion for implementing the @check_charger_available decorator to handle the common error checking pattern.

🧹 Nitpick comments (4)
custom_components/ocpp/api.py (4)

101-142: Consider refactoring service registration to reduce code duplication.

The service registration blocks are repetitive. Consider creating a helper method to register services.

+    def _register_service(
+        self,
+        service_name: str,
+        handler,
+        schema: vol.Schema | None = None,
+        supports_response: SupportsResponse | None = None,
+    ):
+        """Helper method to register services."""
+        kwargs = {"schema": schema} if schema else {}
+        if supports_response:
+            kwargs["supports_response"] = supports_response
+        self.hass.services.async_register(DOMAIN, service_name, handler, **kwargs)

     def __init__(self, hass: HomeAssistant, entry: ConfigEntry):
         # ... existing init code ...
-        self.hass.services.async_register(
-            DOMAIN,
-            csvcs.service_configure.value,
-            self.handle_configure,
-            CONF_SERVICE_DATA_SCHEMA,
-            supports_response=SupportsResponse.OPTIONAL,
-        )
+        self._register_service(
+            csvcs.service_configure.value,
+            self.handle_configure,
+            CONF_SERVICE_DATA_SCHEMA,
+            SupportsResponse.OPTIONAL,
+        )
         # ... repeat for other services

212-237: Enhance error handling specificity.

The generic exception handling could mask specific setup issues. Consider catching and handling specific exceptions.

         try:
             config_flow = False
             for cfg in self.settings.cpids:
                 if cfg.get(cp_id):
                     config_flow = True
                     cp_settings = ChargerSystemSettings(**list(cfg.values())[0])
                     _LOGGER.info(f"Charger match found for {cp_settings.cpid}:{cp_id}")
                     _LOGGER.debug(f"Central settings: {self.settings}")

             if not config_flow:
                 info = {"cp_id": cp_id, "entry": self.entry}
                 await self.hass.config_entries.flow.async_init(
                     DOMAIN, context={"source": SOURCE_INTEGRATION_DISCOVERY}, data=info
                 )
                 return

             self.cpids.update({cp_settings.cpid: cp_id})
             await async_setup_charger(
                 self.hass, self.entry, cs_id=self.id, cpid=cp_settings.cpid, cp_id=cp_id
             )
-        except Exception as e:
-            _LOGGER.error(f"Failed to setup charger {cp_id}: {str(e)}")
+        except KeyError as e:
+            _LOGGER.error(f"Failed to setup charger {cp_id}: Missing configuration key {str(e)}")
+        except ValueError as e:
+            _LOGGER.error(f"Failed to setup charger {cp_id}: Invalid configuration value {str(e)}")
+        except Exception as e:
+            _LOGGER.error(f"Failed to setup charger {cp_id}: Unexpected error {str(e)}")
             return

264-335: Extract common ID lookup logic.

The ID lookup and charge point validation is repeated across multiple methods. Consider extracting this into a helper method.

+    def _get_charge_point(self, id: str):
+        """Get charge point by either cpid or cp_id."""
+        cp_id = self.cpids.get(id, id)
+        return self.charge_points.get(cp_id)

     def get_metric(self, id: str, measurand: str):
         """Return last known value for given measurand."""
-        cp_id = self.cpids.get(id, id)
-        if cp_id in self.charge_points:
-            return self.charge_points[cp_id]._metrics[measurand].value
+        cp = self._get_charge_point(id)
+        if cp:
+            return cp._metrics[measurand].value
         return None

420-444: Improve input validation in charge rate handler.

The method could benefit from explicit validation of mutually exclusive parameters and better error handling for JSON parsing.

     async def handle_set_charge_rate(self, call):
         """Handle the data transfer service call."""
         amps = call.data.get("limit_amps", None)
         watts = call.data.get("limit_watts", None)
         id = call.data.get("conn_id", 0)
         custom_profile = call.data.get("custom_profile", None)
+        # Validate that only one parameter is provided
+        params = sum(1 for p in (custom_profile, watts, amps) if p is not None)
+        if params != 1:
+            raise HomeAssistantError(
+                translation_domain=DOMAIN,
+                translation_key="invalid_params",
+                translation_placeholders={"message": "Exactly one of custom_profile, limit_watts, or limit_amps must be provided"},
+            )
         
         if custom_profile is not None:
             if type(custom_profile) is str:
-                custom_profile = custom_profile.replace("'", '"')
-                custom_profile = json.loads(custom_profile)
+                try:
+                    custom_profile = custom_profile.replace("'", '"')
+                    custom_profile = json.loads(custom_profile)
+                except json.JSONDecodeError as e:
+                    raise HomeAssistantError(
+                        translation_domain=DOMAIN,
+                        translation_key="invalid_json",
+                        translation_placeholders={"message": str(e)},
+                    )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca427dc and 1b79afc.

📒 Files selected for processing (1)
  • custom_components/ocpp/api.py (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run tests
🔇 Additional comments (2)
custom_components/ocpp/api.py (2)

40-82: LGTM! Well-structured service data schemas.

The validation schemas are well-defined with appropriate type checking and required/optional field specifications.


445-474: LGTM! Well-structured configuration handlers.

The configuration handlers are well-implemented with proper error handling and return types.

* add decorator for handle calls

* fix linting

* fix linting

* fix linting
@drc38 drc38 temporarily deployed to continuous-integration January 30, 2025 01:48 — with GitHub Actions Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
custom_components/ocpp/api.py (4)

100-142: Add service documentation.

Consider adding docstrings to describe the purpose and expected behavior of each registered service. This would help users understand how to use these services correctly.

 self.hass.services.async_register(
     DOMAIN,
     csvcs.service_configure.value,
     self.handle_configure,
     CONF_SERVICE_DATA_SCHEMA,
     supports_response=SupportsResponse.OPTIONAL,
+    # Add a docstring like:
+    """Configure a charger setting.
+    
+    This service allows you to configure OCPP settings on the charger.
+    Requires a device ID and OCPP key-value pair.
+    """
 )

Line range hint 150-175: Enhance SSL context error handling.

Consider adding explicit error handling for SSL certificate loading to provide clearer error messages to users.

-            await self.hass.async_add_executor_job(
-                partial(
-                    self.ssl_context.load_cert_chain,
-                    localhost_certfile,
-                    keyfile=localhost_keyfile,
-                )
-            )
+            try:
+                await self.hass.async_add_executor_job(
+                    partial(
+                        self.ssl_context.load_cert_chain,
+                        localhost_certfile,
+                        keyfile=localhost_keyfile,
+                    )
+                )
+            except (FileNotFoundError, ssl.SSLError) as e:
+                _LOGGER.error(f"Failed to load SSL certificates: {str(e)}")
+                raise HomeAssistantError(
+                    translation_domain=DOMAIN,
+                    translation_key="ssl_error",
+                    translation_placeholders={"error": str(e)},
+                )

272-343: Consider extracting the ID resolution logic.

The ID resolution logic is repeated across multiple methods. Consider extracting it into a helper method to improve maintainability.

+    def _resolve_id(self, id: str) -> str:
+        """Resolve either cpid or cp_id to cp_id."""
+        return self.cpids.get(id, id)

-    def get_metric(self, id: str, measurand: str):
-        cp_id = self.cpids.get(id, id)
+    def get_metric(self, id: str, measurand: str):
+        cp_id = self._resolve_id(id)

386-437: Add return type hints to service handlers.

Consider adding return type hints to all service handlers for better code maintainability and IDE support.

-    async def handle_clear_profile(self, call, cp):
+    async def handle_clear_profile(self, call, cp) -> None:

-    async def handle_update_firmware(self, call, cp):
+    async def handle_update_firmware(self, call, cp) -> None:

-    async def handle_get_diagnostics(self, call, cp):
+    async def handle_get_diagnostics(self, call, cp) -> None:

-    async def handle_data_transfer(self, call, cp):
+    async def handle_data_transfer(self, call, cp) -> None:

-    async def handle_set_charge_rate(self, call, cp):
+    async def handle_set_charge_rate(self, call, cp) -> None:
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1b79afc and 4d8f2ee.

📒 Files selected for processing (1)
  • custom_components/ocpp/api.py (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run tests
🔇 Additional comments (4)
custom_components/ocpp/api.py (4)

5-82: LGTM! Well-structured service schemas with appropriate validation.

The service data schemas are well-defined with proper type validation and consistent naming conventions.


Line range hint 83-98: LGTM! Clean initialization with proper settings handling.

The class initialization is well-structured with clear attribute definitions and proper use of type hints.


212-271: LGTM! Robust connection handling with proper error handling.

The implementation follows the suggested improvements from the past review, including proper error handling and logging for charger setup.


369-384: LGTM! Well-implemented decorator for availability checking.

The decorator effectively centralizes the availability check logic and provides consistent error handling across all service handlers.

* revert device registry setup logic

* fix services test

* fix ref

* get new cs in test

* fix linting

* remove id check
@drc38 drc38 temporarily deployed to continuous-integration January 30, 2025 20:45 — with GitHub Actions Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
tests/test_charge_point_v16.py (1)

Line range hint 202-239: Consider adding more descriptive assertion messages.

While the test is well-structured, consider adding descriptive messages to assertions to improve test failure debugging:

-        assert ws2.subprotocol is None
+        assert ws2.subprotocol is None, "Expected no subprotocol for the websocket connection"
custom_components/ocpp/api.py (3)

40-82: Add docstrings to service data schemas.

Consider adding docstrings to explain the purpose and usage of each service data schema. This would improve code maintainability and help other developers understand the expected data format.


101-142: Add error handling for service registration.

Consider wrapping the service registration calls in a try-except block to handle potential registration failures gracefully.

+        try:
             self.hass.services.async_register(
                 DOMAIN,
                 csvcs.service_configure.value,
                 self.handle_configure,
                 CONF_SERVICE_DATA_SCHEMA,
                 supports_response=SupportsResponse.OPTIONAL,
             )
             # ... other service registrations ...
+        except Exception as e:
+            _LOGGER.error(f"Failed to register services: {str(e)}")
+            raise HomeAssistantError(
+                translation_domain=DOMAIN,
+                translation_key="service_registration_failed",
+                translation_placeholders={"error": str(e)},
+            )

212-255: Enhance logging for connection handling.

The connection handling could benefit from more detailed logging to aid in troubleshooting. Consider adding debug logs for key steps in the connection process.

         if cp_id not in self.charge_points:
             try:
+                _LOGGER.debug(f"Starting connection process for charger {cp_id}")
                 config_flow = False
                 for cfg in self.settings.cpids:
                     if cfg.get(cp_id):
                         config_flow = True
                         cp_settings = ChargerSystemSettings(**list(cfg.values())[0])
                         _LOGGER.info(f"Charger match found for {cp_settings.cpid}:{cp_id}")
                         _LOGGER.debug(f"Central settings: {self.settings}")

                 if not config_flow:
+                    _LOGGER.debug(f"No configuration found for {cp_id}, initiating discovery flow")
                     info = {"cp_id": cp_id, "entry": self.entry}
                     await self.hass.config_entries.flow.async_init(
                         DOMAIN,
                         context={"source": SOURCE_INTEGRATION_DISCOVERY},
                         data=info,
                     )
                     return

                 self.cpids.update({cp_settings.cpid: cp_id})
+                _LOGGER.debug(f"Setting up charger {cp_id} with settings: {cp_settings}")
custom_components/ocpp/chargepoint.py (3)

139-147: Add type hints to constructor parameters.

Consider adding type hints to improve code readability and enable better IDE support.

     def __init__(
         self,
-        id,  # is charger cp_id not HA cpid
+        id: str,  # is charger cp_id not HA cpid
         connection,
         version: OcppVersion,
         hass: HomeAssistant,
         entry: ConfigEntry,
         central: CentralSystemSettings,
         charger: ChargerSystemSettings,
     ):

341-373: Extract magic numbers into constants.

Consider extracting the timeout counter threshold into a constant for better maintainability.

+    # Constants for connection monitoring
+    MAX_TIMEOUT_RETRIES = 3
+
     async def monitor_connection(self):
         """Monitor the connection, by measuring the connection latency."""
         self._metrics[cstat.latency_ping.value].unit = "ms"
         self._metrics[cstat.latency_pong.value].unit = "ms"
         connection = self._connection
         timeout_counter = 0
         while connection.state is State.OPEN:
             try:
                 await asyncio.sleep(self.cs_settings.websocket_ping_interval)
                 # ... rest of the code ...
                 if timeout_counter > self.cs_settings.websocket_ping_tries:
                     _LOGGER.debug(
                         f"Connection to '{self.id}' timed out after '{self.cs_settings.websocket_ping_tries}' ping tries",
                     )
                     raise timeout_exception

Line range hint 505-597: Add detailed documentation for phase data processing.

The phase data processing logic is complex and would benefit from detailed documentation explaining the calculations and assumptions.

     def process_phases(self, data: list[MeasurandValue]):
-        """Process phase data from meter values ."""
+        """Process phase data from meter values.
+        
+        This method processes electrical measurements for three-phase power systems:
+        - Handles line-to-neutral and line-to-line voltage measurements
+        - Converts line-to-line voltages to line-to-neutral using √3
+        - Aggregates current and power measurements across phases
+        - Handles special cases for non-standard reporting formats
+        
+        Args:
+            data: List of MeasurandValue objects containing phase measurements
+        
+        Note:
+            - Line-to-neutral voltages are averaged
+            - Line-to-line voltages are converted to line-to-neutral
+            - Current and power values are summed across phases
+        """
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d8f2ee and cac5285.

📒 Files selected for processing (8)
  • custom_components/ocpp/__init__.py (2 hunks)
  • custom_components/ocpp/api.py (5 hunks)
  • custom_components/ocpp/button.py (3 hunks)
  • custom_components/ocpp/chargepoint.py (7 hunks)
  • custom_components/ocpp/number.py (4 hunks)
  • custom_components/ocpp/sensor.py (5 hunks)
  • custom_components/ocpp/switch.py (5 hunks)
  • tests/test_charge_point_v16.py (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • custom_components/ocpp/sensor.py
  • custom_components/ocpp/button.py
🧰 Additional context used
📓 Learnings (1)
custom_components/ocpp/api.py (1)
Learnt from: drc38
PR: lbbrhzn/ocpp#1498
File: custom_components/ocpp/api.py:0-0
Timestamp: 2025-01-30T01:49:36.840Z
Learning: The user (drc38) is receptive to code quality improvements and refactoring suggestions that reduce code duplication and improve maintainability.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run tests
🔇 Additional comments (14)
custom_components/ocpp/number.py (3)

78-78: LGTM! Class and parameter renaming.

The class has been appropriately renamed from OcppNumber to ChargePointNumber, and the constructor parameter has been updated from cp_id to cpid for consistency.

Also applies to: 88-92


124-127: Implement or remove the commented-out available property.

The commented-out code suggests incomplete implementation.


60-73: 🛠️ Refactor suggestion

Verify the iteration logic for multiple chargers.

The code now supports multiple chargers by iterating over entry.data[CONF_CPIDS]. However, the maximum current value is being set from the entry level rather than from each charger's settings.

Consider updating the code to use charger-specific settings:

 for charger in entry.data[CONF_CPIDS]:
     cp_id_settings = list(charger.values())[0]
     cpid = cp_id_settings[CONF_CPID]
 
     for ent in NUMBERS:
         if ent.key == "maximum_current":
-            ent.initial_value = entry.data.get(
-                CONF_MAX_CURRENT, DEFAULT_MAX_CURRENT
-            )
-            ent.native_max_value = entry.data.get(
-                CONF_MAX_CURRENT, DEFAULT_MAX_CURRENT
-            )
+            ent.initial_value = cp_id_settings.get(
+                CONF_MAX_CURRENT, DEFAULT_MAX_CURRENT
+            )
+            ent.native_max_value = cp_id_settings.get(
+                CONF_MAX_CURRENT, DEFAULT_MAX_CURRENT
+            )
         cpx = ChargePointNumber(hass, central_system, cpid, ent)
         entities.append(cpx)
custom_components/ocpp/switch.py (2)

69-75: LGTM! Proper handling of multiple chargers.

The switch setup correctly iterates over multiple chargers and creates switch entities for each one.


89-93: LGTM! Constructor parameter update.

The constructor parameter has been appropriately updated from cp_id to cpid for consistency.

custom_components/ocpp/__init__.py (3)

108-126: LGTM! Improved device registry handling.

The code now properly creates both central system and charger devices with appropriate identifiers and relationships.


206-221: 🛠️ Refactor suggestion

Add timeout and error handling to unload process.

The unload entry function could be improved with timeout and error handling.

Apply this diff to improve the unload process:

 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
     """Handle removal of an entry."""
     unloaded = False
     if DOMAIN in hass.data:
         if entry.entry_id in hass.data[DOMAIN]:
             central_sys = hass.data[DOMAIN][entry.entry_id]
+            try:
+                with async_timeout.timeout(10):
                     central_sys._server.close()
                     await central_sys._server.wait_closed()
+            except asyncio.TimeoutError:
+                _LOGGER.error("Timeout waiting for server to close")
+            except Exception as err:
+                _LOGGER.error("Error closing server: %s", err)
 
+            # Document connection state handling
+            _LOGGER.debug("Current connections: %d", central_sys.connections)
             for service in hass.services.async_services_for_domain(DOMAIN):
-                hass.services.async_remove(DOMAIN, service)
+                try:
+                    hass.services.async_remove(DOMAIN, service)
+                except Exception as err:
+                    _LOGGER.error("Error removing service %s: %s", service, err)

Likely invalid or redundant comment.


138-198: 🛠️ Refactor suggestion

Add data validation and error handling to migration logic.

The migration function could be improved with better error handling and data validation.

Apply this diff to improve the migration function:

 async def async_migrate_entry(hass, config_entry: ConfigEntry):
     """Migrate old entry."""
+    try:
         _LOGGER.debug(
             "Migrating configuration from version %s.%s",
             config_entry.version,
             config_entry.minor_version,
         )
 
         if config_entry.version > 2:
             return False
 
         if config_entry.version == 1:
             old_data = {**config_entry.data}
+            if not isinstance(old_data, dict):
+                _LOGGER.error("Invalid configuration data type")
+                return False
+
             csid_data = {}
             cpid_data = {}
             # ... existing code ...
 
             new_data = csid_data
             cp_id = hass.states.get(f"sensor.{cpid_data[CONF_CPID]}_id")
             if cp_id is None:
+                _LOGGER.error("Charger state not found")
                 return False
             new_data.update({CONF_CPIDS: [{cp_id: cpid_data}]})
 
             hass.config_entries.async_update_entry(
                 config_entry, data=new_data, minor_version=0, version=2
             )
+    except Exception as e:
+        _LOGGER.error("Migration failed: %s", str(e))
+        return False
 
     _LOGGER.debug(
         "Migration to configuration version %s.%s successful",
         config_entry.version,
         config_entry.minor_version,
     )
 
     return True

Likely invalid or redundant comment.

tests/test_charge_point_v16.py (6)

4-4: LGTM! Well-organized imports and constants.

The new imports and constants are appropriately used throughout the code. The SERVICES_ERROR list is a good addition for testing error scenarios with limited profiles.

Also applies to: 9-9, 67-73


76-167: LGTM! Well-structured test helper functions.

The helper functions are well-organized with:

  • Clear parameter naming (cpid instead of cs)
  • Comprehensive test coverage
  • Proper error handling
  • Good reusability across test cases

Line range hint 275-336: LGTM! Well-implemented value restoration test.

The test effectively verifies the central system's ability to restore values after simulated data loss. Good use of assertions to validate the restored values.


Line range hint 348-542: LGTM! Comprehensive normal operation tests.

The test suite effectively covers:

  • Normal charger operations
  • Multi-charger scenarios
  • Different energy measurement units
  • Various meter value formats

553-621: LGTM! Thorough error scenario testing.

Good coverage of error scenarios with:

  • Rejection messages
  • Service unavailability
  • Error handling with HomeAssistantError

Line range hint 624-1183: LGTM! Well-implemented ChargePoint test class.

The class provides:

  • Comprehensive coverage of OCPP operations
  • Good error handling with accept flag
  • Clear method documentation
  • Proper use of decorators for action handlers

custom_components/ocpp/api.py Show resolved Hide resolved
custom_components/ocpp/chargepoint.py Show resolved Hide resolved
@drc38 drc38 temporarily deployed to continuous-integration January 30, 2025 22:44 — with GitHub Actions Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
custom_components/ocpp/config_flow.py (1)

Line range hint 78-104: Add validation for numeric fields and CPID format.

Consider adding validation rules to ensure data integrity:

 STEP_USER_CP_DATA_SCHEMA = vol.Schema(
     {
-        vol.Required(CONF_CPID, default=DEFAULT_CPID): str,
+        vol.Required(CONF_CPID, default=DEFAULT_CPID): vol.Match(r'^[a-zA-Z0-9-_]+$'),
-        vol.Required(CONF_MAX_CURRENT, default=DEFAULT_MAX_CURRENT): int,
+        vol.Required(CONF_MAX_CURRENT, default=DEFAULT_MAX_CURRENT): vol.All(
+            int, vol.Range(min=0, max=100)
+        ),
-        vol.Required(CONF_METER_INTERVAL, default=DEFAULT_METER_INTERVAL): int,
+        vol.Required(CONF_METER_INTERVAL, default=DEFAULT_METER_INTERVAL): vol.All(
+            int, vol.Range(min=0)
+        ),
-        vol.Required(CONF_IDLE_INTERVAL, default=DEFAULT_IDLE_INTERVAL): int,
+        vol.Required(CONF_IDLE_INTERVAL, default=DEFAULT_IDLE_INTERVAL): vol.All(
+            int, vol.Range(min=0)
+        ),
         # ... rest of the schema
     }
 )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cac5285 and 79c78e9.

📒 Files selected for processing (1)
  • custom_components/ocpp/config_flow.py (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Run tests
🔇 Additional comments (4)
custom_components/ocpp/config_flow.py (4)

Line range hint 3-77: LGTM! Clean separation of central system configuration schema.

The separation of configuration schemas and the addition of necessary imports look good. This provides a clear distinction between central system and charger-specific configurations.


105-118: 🛠️ Refactor suggestion

Initialize all class attributes.

Some attributes are still uninitialized:

     def __init__(self):
         """Initialize."""
         self._data: dict[str, Any] = {}
-        self._cp_id: str
-        self._entry: ConfigEntry
+        self._cp_id: str = ""
+        self._entry: ConfigEntry | None = None
         self._measurands: str = ""

Likely invalid or redundant comment.


179-196: 🛠️ Refactor suggestion

Improve error handling in measurands step.

The error handling for measurands selection needs improvement:

-            if not set(selected_measurands).issubset(set(MEASURANDS)):
+            if not selected_measurands:
                 errors["base"] = "no_measurands_selected"
                 return self.async_show_form(
                     step_id="measurands",
                     data_schema=STEP_USER_MEASURANDS_SCHEMA,
                     errors=errors,
                 )
+            if not set(selected_measurands).issubset(set(MEASURANDS)):
+                errors["base"] = "invalid_measurands"
+                return self.async_show_form(
+                    step_id="measurands",
+                    data_schema=STEP_USER_MEASURANDS_SCHEMA,
+                    errors=errors,
+                )
             else:
                 self._measurands = ",".join(selected_measurands)

Likely invalid or redundant comment.


155-168: ⚠️ Potential issue

Fix potential race condition in charger configuration.

The current implementation might have race conditions when multiple chargers are being configured simultaneously:

 if user_input is not None:
     # Don't allow duplicate cpids to be used
     self._async_abort_entries_match({CONF_CPID: user_input[CONF_CPID]})
-    self._data[CONF_CPIDS].append({self._cp_id: user_input})
+    # Create a new copy of the data to avoid race conditions
+    updated_data = {**self._data}
+    if CONF_CPIDS not in updated_data:
+        updated_data[CONF_CPIDS] = []
+    updated_data[CONF_CPIDS].append({self._cp_id: user_input})
     if user_input[CONF_MONITORED_VARIABLES_AUTOCONFIG]:
-        self._data[CONF_CPIDS][-1][self._cp_id][CONF_MONITORED_VARIABLES] = (
+        updated_data[CONF_CPIDS][-1][self._cp_id][CONF_MONITORED_VARIABLES] = (
             DEFAULT_MONITORED_VARIABLES
         )
         return self.async_update_reload_and_abort(
             self._entry,
-            data_updates=self._data,
+            data_updates=updated_data,
         )

Likely invalid or redundant comment.

@drc38
Copy link
Collaborator Author

drc38 commented Jan 31, 2025

@rbb-dev this version works for me https://github.com/drc38/ocpp/releases/tag/0.8.13 if there are issues for you check the config entry output to make sure the chargers have been entered and saved by HA.

@drc38 drc38 requested a review from lbbrhzn February 4, 2025 08:20
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