Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion custom_components/ocpp/chargepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,10 @@ async def async_update_device_info(

def _register_boot_notification(self):
if self.triggered_boot_notification is False:
self.hass.async_create_task(self.notify_ha(f"Charger {self.id} rebooted"))
if self.cs_settings.enable_reboot_notifications:
self.hass.async_create_task(
self.notify_ha(f"Charger {self.id} rebooted")
)
if not self.post_connect_success:
self.hass.async_create_task(self.post_connect())

Expand Down
159 changes: 135 additions & 24 deletions custom_components/ocpp/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
ConfigFlow,
ConfigFlowResult,
CONN_CLASS_LOCAL_PUSH,
OptionsFlow,
)
from homeassistant.helpers import config_validation as cv
import voluptuous as vol
Expand All @@ -14,6 +15,7 @@
CONF_CPID,
CONF_CPIDS,
CONF_CSID,
CONF_ENABLE_REBOOT_NOTIFICATIONS,
CONF_FORCE_SMART_CHARGING,
CONF_HOST,
CONF_IDLE_INTERVAL,
Expand All @@ -33,6 +35,7 @@
CONF_WEBSOCKET_PING_TRIES,
DEFAULT_CPID,
DEFAULT_CSID,
DEFAULT_ENABLE_REBOOT_NOTIFICATIONS,
DEFAULT_FORCE_SMART_CHARGING,
DEFAULT_HOST,
DEFAULT_IDLE_INTERVAL,
Expand All @@ -55,29 +58,6 @@
MEASURANDS,
)

STEP_USER_CS_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
vol.Required(CONF_SSL, default=DEFAULT_SSL): bool,
vol.Required(CONF_SSL_CERTFILE_PATH, default=DEFAULT_SSL_CERTFILE_PATH): str,
vol.Required(CONF_SSL_KEYFILE_PATH, default=DEFAULT_SSL_KEYFILE_PATH): str,
vol.Required(CONF_CSID, default=DEFAULT_CSID): vol.All(str, vol.Length(max=20)),
vol.Required(
CONF_WEBSOCKET_CLOSE_TIMEOUT, default=DEFAULT_WEBSOCKET_CLOSE_TIMEOUT
): int,
vol.Required(
CONF_WEBSOCKET_PING_TRIES, default=DEFAULT_WEBSOCKET_PING_TRIES
): int,
vol.Required(
CONF_WEBSOCKET_PING_INTERVAL, default=DEFAULT_WEBSOCKET_PING_INTERVAL
): int,
vol.Required(
CONF_WEBSOCKET_PING_TIMEOUT, default=DEFAULT_WEBSOCKET_PING_TIMEOUT
): int,
}
)

STEP_USER_CP_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_CPID, default=DEFAULT_CPID): str,
Expand Down Expand Up @@ -105,13 +85,85 @@
)


def _get_cs_data_schema(config: dict[str, Any] | None = None) -> vol.Schema:
"""Build the central system schema with config-backed defaults."""

config = config or {}

return vol.Schema(
{
vol.Required(CONF_HOST, default=config.get(CONF_HOST, DEFAULT_HOST)): str,
vol.Required(CONF_PORT, default=config.get(CONF_PORT, DEFAULT_PORT)): int,
vol.Required(CONF_SSL, default=config.get(CONF_SSL, DEFAULT_SSL)): bool,
vol.Required(
CONF_SSL_CERTFILE_PATH,
default=config.get(
CONF_SSL_CERTFILE_PATH,
DEFAULT_SSL_CERTFILE_PATH,
),
): str,
vol.Required(
CONF_SSL_KEYFILE_PATH,
default=config.get(
CONF_SSL_KEYFILE_PATH,
DEFAULT_SSL_KEYFILE_PATH,
),
): str,
vol.Required(
CONF_CSID,
default=config.get(CONF_CSID, DEFAULT_CSID),
): vol.All(str, vol.Length(max=20)),
vol.Required(
CONF_ENABLE_REBOOT_NOTIFICATIONS,
default=config.get(
CONF_ENABLE_REBOOT_NOTIFICATIONS,
DEFAULT_ENABLE_REBOOT_NOTIFICATIONS,
),
): bool,
vol.Required(
CONF_WEBSOCKET_CLOSE_TIMEOUT,
default=config.get(
CONF_WEBSOCKET_CLOSE_TIMEOUT,
DEFAULT_WEBSOCKET_CLOSE_TIMEOUT,
),
): int,
vol.Required(
CONF_WEBSOCKET_PING_TRIES,
default=config.get(
CONF_WEBSOCKET_PING_TRIES,
DEFAULT_WEBSOCKET_PING_TRIES,
),
): int,
vol.Required(
CONF_WEBSOCKET_PING_INTERVAL,
default=config.get(
CONF_WEBSOCKET_PING_INTERVAL,
DEFAULT_WEBSOCKET_PING_INTERVAL,
),
): int,
vol.Required(
CONF_WEBSOCKET_PING_TIMEOUT,
default=config.get(
CONF_WEBSOCKET_PING_TIMEOUT,
DEFAULT_WEBSOCKET_PING_TIMEOUT,
),
): int,
}
)


class ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OCPP."""

VERSION = 2
MINOR_VERSION = 1
CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH

@staticmethod
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Return the options flow for this config entry."""
return OcppOptionsFlowHandler(config_entry)

def __init__(self):
"""Initialize."""
self._data: dict[str, Any] = {}
Expand All @@ -134,7 +186,32 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult:

return self.async_show_form(
step_id="user",
data_schema=STEP_USER_CS_DATA_SCHEMA,
data_schema=_get_cs_data_schema(),
errors=errors,
description_placeholders={"docs_url": "https://github.com/lbbrhzn/ocpp"},
)

async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration from the integrations page."""

errors: dict[str, str] = {}
entry = self._get_reconfigure_entry()

if user_input is not None:
if user_input[CONF_PORT] != entry.data[CONF_PORT]:
self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]})

return self.async_update_reload_and_abort(
entry,
title=user_input[CONF_CSID],
data_updates=user_input,
)

return self.async_show_form(
step_id="reconfigure",
data_schema=_get_cs_data_schema(entry.data),
errors=errors,
description_placeholders={"docs_url": "https://github.com/lbbrhzn/ocpp"},
)
Expand Down Expand Up @@ -231,3 +308,37 @@ async def async_step_measurands(self, user_input=None):
data_schema=STEP_USER_MEASURANDS_SCHEMA,
errors=errors,
)


class OcppOptionsFlowHandler(OptionsFlow):
"""Handle OCPP options shown from the integration page gear button."""

def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self._config_entry = config_entry

async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the OCPP integration settings."""

errors: dict[str, str] = {}

if user_input is not None:
if user_input[CONF_PORT] != self._config_entry.data[CONF_PORT]:
self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]})
Comment on lines +327 to +329

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Replace _async_abort_entries_match to prevent AttributeError.

Unlike ConfigFlow, OptionsFlow subclasses do not inherit the _async_abort_entries_match method in Home Assistant. If a user changes the port in the options flow, executing this line will raise an AttributeError and crash the flow.

You can manually check for port conflicts by iterating over existing config entries in the domain.

🐛 Proposed fix
         if user_input is not None:
             if user_input[CONF_PORT] != self._config_entry.data[CONF_PORT]:
-                self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]})
+                for entry in self.hass.config_entries.async_entries(self._config_entry.domain):
+                    if (
+                        entry.entry_id != self._config_entry.entry_id
+                        and entry.data.get(CONF_PORT) == user_input[CONF_PORT]
+                    ):
+                        return self.async_abort(reason="already_configured")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if user_input is not None:
if user_input[CONF_PORT] != self._config_entry.data[CONF_PORT]:
self._async_abort_entries_match({CONF_PORT: user_input[CONF_PORT]})
if user_input is not None:
if user_input[CONF_PORT] != self._config_entry.data[CONF_PORT]:
for entry in self.hass.config_entries.async_entries(self._config_entry.domain):
if (
entry.entry_id != self._config_entry.entry_id
and entry.data.get(CONF_PORT) == user_input[CONF_PORT]
):
return self.async_abort(reason="already_configured")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@custom_components/ocpp/config_flow.py` around lines 327 - 329, Replace the
`_async_abort_entries_match` call in the options-flow port-change handling with
an explicit iteration over existing config entries, aborting when another entry
in the domain already uses the requested port. Preserve the current behavior of
allowing the unchanged port and avoid relying on the unavailable OptionsFlow
method.


self.hass.config_entries.async_update_entry(
self._config_entry,
title=user_input[CONF_CSID],
data={**self._config_entry.data, **user_input},
)
self.hass.config_entries.async_schedule_reload(self._config_entry.entry_id)
return self.async_create_entry(title="", data=self._config_entry.options)

return self.async_show_form(
step_id="init",
data_schema=_get_cs_data_schema(self._config_entry.data),
errors=errors,
description_placeholders={"docs_url": "https://github.com/lbbrhzn/ocpp"},
)
3 changes: 3 additions & 0 deletions custom_components/ocpp/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CONF_CPID = "cpid"
CONF_CPIDS = "cpids"
CONF_CSID = "csid"
CONF_ENABLE_REBOOT_NOTIFICATIONS = "enable_reboot_notifications"
CONF_DEFAULT_AUTH_STATUS = "default_authorization_status"
CONF_HOST = ha.CONF_HOST
CONF_ID_TAG = "id_tag"
Expand Down Expand Up @@ -44,6 +45,7 @@
DATA_UPDATED = "ocpp_data_updated"
DEFAULT_CSID = "central"
DEFAULT_CPID = "charger"
DEFAULT_ENABLE_REBOOT_NOTIFICATIONS = True
DEFAULT_HOST = "0.0.0.0"
DEFAULT_MAX_CURRENT = 32
DEFAULT_NUM_CONNECTORS = 1
Expand Down Expand Up @@ -170,6 +172,7 @@ class CentralSystemSettings:
websocket_ping_interval: int
websocket_ping_timeout: int
websocket_ping_tries: int
enable_reboot_notifications: bool = DEFAULT_ENABLE_REBOOT_NOTIFICATIONS
cpids: list = field(default_factory=list) # holds cpid config flow settings
subprotocols: list = field(default_factory=lambda: DEFAULT_SUBPROTOCOLS)

Expand Down
22 changes: 22 additions & 0 deletions custom_components/ocpp/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"ssl_certfile_path": "Path to SSL certificate or (None)",
"ssl_keyfile_path": "Path to SSL key or (None)",
"csid": "Central system identity",
"enable_reboot_notifications": "Enable charger reboot notifications",
"websocket_close_timeout": "Websocket close timeout (seconds)",
"websocket_ping_tries": "Websocket successive times to try connection before closing",
"websocket_ping_interval": "Websocket ping interval (seconds)",
Expand Down Expand Up @@ -69,6 +70,27 @@
"reauth_successful": "New charger configured"
}
},
"options": {
"step": {
"init": {
"title": "OCPP Central System Settings",
"description": "If you need help with the configuration have a look [here]({docs_url})",
"data": {
"host": "Central system host address",
"port": "Central system port number",
"ssl": "Secure connection",
"ssl_certfile_path": "Path to SSL certificate or (None)",
"ssl_keyfile_path": "Path to SSL key or (None)",
"csid": "Central system identity",
"enable_reboot_notifications": "Enable charger reboot notifications",
"websocket_close_timeout": "Websocket close timeout (seconds)",
"websocket_ping_tries": "Websocket successive times to try connection before closing",
"websocket_ping_interval": "Websocket ping interval (seconds)",
"websocket_ping_timeout": "Websocket ping timeout (seconds)"
}
}
}
},
"exceptions": {
"invalid_ocpp_key": {
"message": "Invalid OCPP key"
Expand Down
1 change: 1 addition & 0 deletions custom_components/ocpp/translations/i-default.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"ssl_certfile_path": "Path to SSL certificate or (None)",
"ssl_keyfile_path": "Path to SSL key or (None)",
"csid": "Central system identity",
"enable_reboot_notifications": "Enable charger reboot notifications",
"websocket_close_timeout": "Websocket close timeout (seconds)",
"websocket_ping_tries": "Websocket successive times to try connection before closing",
"websocket_ping_interval": "Websocket ping interval (seconds)",
Expand Down
6 changes: 5 additions & 1 deletion docs/support.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Support
=======

- [General](#general)

Check warning on line 4 in docs/support.md

View workflow job for this annotation

GitHub Actions / docs

'myst' cross-reference target not found: 'general' [myst.xref_missing]
- [FAQ](#faq)

Check warning on line 5 in docs/support.md

View workflow job for this annotation

GitHub Actions / docs

Multiple matches found for 'faq': sphinx:std:label:faq, sphinx:std:doc:faq [myst.iref_ambiguous]
- [too many notifications in home assistant](#too-many-notifications-in-home-assistant)

Check warning on line 6 in docs/support.md

View workflow job for this annotation

GitHub Actions / docs

'myst' cross-reference target not found: 'too-many-notifications-in-home-assistant' [myst.xref_missing]

## General

Expand All @@ -13,7 +13,11 @@

### too many notifications in home assistant

The OCPP sends a notification when the charger is rebooted. This can be due to a bad network connection. The notifications can be managed with automations in home assistant. (see https://github.com/lbbrhzn/ocpp/discussions/938)
The OCPP integration can send a notification when the charger is rebooted. This can be due to a bad network connection.

If you do not want these notifications, you can now disable them in the integration settings by turning off the reboot notification setting for the central system entry.

If you prefer to keep the notifications enabled but automatically dismiss them (e.g. to only silence nightly reboots), they can also be managed with automations in Home Assistant. (see https://github.com/lbbrhzn/ocpp/discussions/938)

Example:

Expand Down
6 changes: 6 additions & 0 deletions tests/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
CONF_CPID,
CONF_CPIDS,
CONF_CSID,
CONF_ENABLE_REBOOT_NOTIFICATIONS,
CONF_FORCE_SMART_CHARGING,
CONF_HOST,
CONF_IDLE_INTERVAL,
Expand All @@ -21,6 +22,7 @@
CONF_WEBSOCKET_PING_INTERVAL,
CONF_WEBSOCKET_PING_TIMEOUT,
CONF_WEBSOCKET_PING_TRIES,
DEFAULT_ENABLE_REBOOT_NOTIFICATIONS,
DEFAULT_MONITORED_VARIABLES,
)

Expand All @@ -31,6 +33,7 @@
CONF_SSL_CERTFILE_PATH: "/tests/fullchain.pem",
CONF_SSL_KEYFILE_PATH: "/tests/privkey.pem",
CONF_CSID: "test_csid_flow",
CONF_ENABLE_REBOOT_NOTIFICATIONS: DEFAULT_ENABLE_REBOOT_NOTIFICATIONS,
CONF_WEBSOCKET_CLOSE_TIMEOUT: 1,
CONF_WEBSOCKET_PING_TRIES: 0,
CONF_WEBSOCKET_PING_INTERVAL: 1,
Expand All @@ -55,6 +58,7 @@
CONF_SSL: False,
CONF_SSL_CERTFILE_PATH: "/tests/fullchain.pem",
CONF_SSL_KEYFILE_PATH: "/tests/privkey.pem",
CONF_ENABLE_REBOOT_NOTIFICATIONS: DEFAULT_ENABLE_REBOOT_NOTIFICATIONS,
CONF_WEBSOCKET_CLOSE_TIMEOUT: 1,
CONF_WEBSOCKET_PING_TRIES: 0,
CONF_WEBSOCKET_PING_INTERVAL: 1,
Expand Down Expand Up @@ -83,6 +87,7 @@
CONF_SSL: False,
CONF_SSL_CERTFILE_PATH: "/tests/fullchain.pem",
CONF_SSL_KEYFILE_PATH: "/tests/privkey.pem",
CONF_ENABLE_REBOOT_NOTIFICATIONS: DEFAULT_ENABLE_REBOOT_NOTIFICATIONS,
CONF_WEBSOCKET_CLOSE_TIMEOUT: 1,
CONF_WEBSOCKET_PING_TRIES: 0,
CONF_WEBSOCKET_PING_INTERVAL: 1,
Expand Down Expand Up @@ -181,6 +186,7 @@
CONF_SSL: False,
CONF_SSL_CERTFILE_PATH: "/tests/fullchain.pem",
CONF_SSL_KEYFILE_PATH: "/tests/privkey.pem",
CONF_ENABLE_REBOOT_NOTIFICATIONS: DEFAULT_ENABLE_REBOOT_NOTIFICATIONS,
CONF_WEBSOCKET_CLOSE_TIMEOUT: 1,
CONF_WEBSOCKET_PING_TRIES: 0,
CONF_WEBSOCKET_PING_INTERVAL: 1,
Expand Down
Loading
Loading