Skip to content

Commit

Permalink
Allow to disable autodetection of measurands (#1292)
Browse files Browse the repository at this point in the history
* Allow to disable autodetection of measurands

- Add config option to detect measurands automatically, set default to true

- Restrict checked measurands to the ones provided by users

Disabling autodetection works around ABB Terra AC bugs as in #1275

* Validate user provided list of measurands

* Fix linting

* Add quirk to set charger measurands to the list provided by user

* Use form to select measurands

If measurands are not autodetected, allow user to select them. In part, reverts
earlier commit 64dab6e

* Add OCPP measurands tags to user form

This allows users to consult technical documentation of their chargers
and select measurands based on OCPP tags. In addition, it allows us to
document working tags for specific chargers in HA OCPP documentation

* Document ABB Terra AC chargers configuration

* Drop empty line

* Add new configuration to tests

* Set charger measurands to enabled ones if not autodetecting

As a workaround for bugs in some chargers, set measurands to user-provided list
while avoiding querying current settings from the charger

* Update documentation with simplified ABB configuration

---------

Co-authored-by: lbbrhzn <[email protected]>
  • Loading branch information
rinigus and lbbrhzn authored Sep 9, 2024
1 parent bdcfd64 commit b963bd0
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 157 deletions.
71 changes: 44 additions & 27 deletions custom_components/ocpp/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry, entity_component, entity_registry
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
import websockets.protocol
import websockets.server

from ocpp.exceptions import NotImplementedError
from ocpp.messages import CallError
from ocpp.routing import on
Expand Down Expand Up @@ -51,6 +47,9 @@
UnitOfMeasure,
UnlockStatus,
)
import voluptuous as vol
import websockets.protocol
import websockets.server

from .const import (
CONF_AUTH_LIST,
Expand All @@ -64,6 +63,7 @@
CONF_IDLE_INTERVAL,
CONF_METER_INTERVAL,
CONF_MONITORED_VARIABLES,
CONF_MONITORED_VARIABLES_AUTOCONFIG,
CONF_PORT,
CONF_SKIP_SCHEMA_VALIDATION,
CONF_SSL,
Expand All @@ -83,6 +83,7 @@
DEFAULT_IDLE_INTERVAL,
DEFAULT_MEASURAND,
DEFAULT_METER_INTERVAL,
DEFAULT_MONITORED_VARIABLES_AUTOCONFIG,
DEFAULT_PORT,
DEFAULT_POWER_UNIT,
DEFAULT_SKIP_SCHEMA_VALIDATION,
Expand Down Expand Up @@ -469,30 +470,46 @@ async def handle_set_charge_rate(call):
all_measurands = self.entry.data.get(
CONF_MONITORED_VARIABLES, DEFAULT_MEASURAND
)
autodetect_measurands = self.entry.data.get(
CONF_MONITORED_VARIABLES_AUTOCONFIG,
DEFAULT_MONITORED_VARIABLES_AUTOCONFIG,
)

key = ckey.meter_values_sampled_data.value
try:
chgr_measurands = await self.get_configuration(key)
except Exception:
_LOGGER.debug(
f"'{self.id}' had error while returning measurands, ignoring"
)
chgr_measurands = all_measurands

accepted_measurands = []
cfg_ok = [
ConfigurationStatus.accepted,
ConfigurationStatus.reboot_required,
]

for measurand in all_measurands.split(","):
_LOGGER.debug(f"'{self.id}' trying measurand: '{measurand}'")
req = call.ChangeConfiguration(key=key, value=measurand)
resp = await self.call(req)
if resp.status in cfg_ok:
_LOGGER.debug(f"'{self.id}' adding measurand: '{measurand}'")
accepted_measurands.append(measurand)

accepted_measurands = ",".join(accepted_measurands)

if autodetect_measurands:
accepted_measurands = []
cfg_ok = [
ConfigurationStatus.accepted,
ConfigurationStatus.reboot_required,
]

for measurand in all_measurands.split(","):
_LOGGER.debug(f"'{self.id}' trying measurand: '{measurand}'")
req = call.ChangeConfiguration(key=key, value=measurand)
resp = await self.call(req)
if resp.status in cfg_ok:
_LOGGER.debug(f"'{self.id}' adding measurand: '{measurand}'")
accepted_measurands.append(measurand)

accepted_measurands = ",".join(accepted_measurands)
else:
accepted_measurands = all_measurands

# Quirk:
# Workaround for a bug on chargers that have invalid MeterValuesSampledData
# configuration and reboot while the server requests MeterValuesSampledData.
# By setting the configuration directly without checking current configuration
# as done when calling self.configure, the server avoids charger reboot.
# Corresponding issue: https://github.com/lbbrhzn/ocpp/issues/1275
if len(accepted_measurands) > 0:
req = call.ChangeConfiguration(key=key, value=accepted_measurands)
resp = await self.call(req)
_LOGGER.debug(
f"'{self.id}' measurands set manually to {accepted_measurands}"
)

chgr_measurands = await self.get_configuration(key)

if len(accepted_measurands) > 0:
_LOGGER.debug(
Expand Down
44 changes: 39 additions & 5 deletions custom_components/ocpp/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CONF_MAX_CURRENT,
CONF_METER_INTERVAL,
CONF_MONITORED_VARIABLES,
CONF_MONITORED_VARIABLES_AUTOCONFIG,
CONF_PORT,
CONF_SKIP_SCHEMA_VALIDATION,
CONF_SSL,
Expand All @@ -27,8 +28,10 @@
DEFAULT_HOST,
DEFAULT_IDLE_INTERVAL,
DEFAULT_MAX_CURRENT,
DEFAULT_MEASURAND,
DEFAULT_METER_INTERVAL,
DEFAULT_MONITORED_VARIABLES,
DEFAULT_MONITORED_VARIABLES_AUTOCONFIG,
DEFAULT_PORT,
DEFAULT_SKIP_SCHEMA_VALIDATION,
DEFAULT_SSL,
Expand All @@ -39,6 +42,7 @@
DEFAULT_WEBSOCKET_PING_TIMEOUT,
DEFAULT_WEBSOCKET_PING_TRIES,
DOMAIN,
MEASURANDS,
)

STEP_USER_DATA_SCHEMA = vol.Schema(
Expand All @@ -52,8 +56,9 @@
vol.Required(CONF_CPID, default=DEFAULT_CPID): str,
vol.Required(CONF_MAX_CURRENT, default=DEFAULT_MAX_CURRENT): int,
vol.Required(
CONF_MONITORED_VARIABLES, default=DEFAULT_MONITORED_VARIABLES
): str,
CONF_MONITORED_VARIABLES_AUTOCONFIG,
default=DEFAULT_MONITORED_VARIABLES_AUTOCONFIG,
): bool,
vol.Required(CONF_METER_INTERVAL, default=DEFAULT_METER_INTERVAL): int,
vol.Required(CONF_IDLE_INTERVAL, default=DEFAULT_IDLE_INTERVAL): int,
vol.Required(
Expand All @@ -77,6 +82,13 @@
}
)

STEP_USER_MEASURANDS_SCHEMA = vol.Schema(
{
vol.Required(m, default=(True if m == DEFAULT_MEASURAND else False)): bool
for m in MEASURANDS
}
)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OCPP."""
Expand All @@ -93,11 +105,33 @@ async def async_step_user(self, user_input=None):
errors: dict[str, str] = {}

if user_input is not None:
# Todo: validate the user input
self._data = user_input
self._data[CONF_MONITORED_VARIABLES] = DEFAULT_MONITORED_VARIABLES
return self.async_create_entry(title=self._data[CONF_CSID], data=self._data)
if user_input[CONF_MONITORED_VARIABLES_AUTOCONFIG]:
self._data[CONF_MONITORED_VARIABLES] = DEFAULT_MONITORED_VARIABLES
return self.async_create_entry(
title=self._data[CONF_CSID], data=self._data
)
return await self.async_step_measurands()

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

async def async_step_measurands(self, user_input=None):
"""Select the measurands to be shown."""

errors: dict[str, str] = {}
if user_input is not None:
selected_measurands = [m for m, value in user_input.items() if value]
if set(selected_measurands).issubset(set(MEASURANDS)):
self._data[CONF_MONITORED_VARIABLES] = ",".join(selected_measurands)
return self.async_create_entry(
title=self._data[CONF_CSID], data=self._data
)
else:
errors["base"] = "measurand"
return self.async_show_form(
step_id="measurands",
data_schema=STEP_USER_MEASURANDS_SCHEMA,
errors=errors,
)
3 changes: 2 additions & 1 deletion custom_components/ocpp/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import homeassistant.components.input_number as input_number
from homeassistant.components.sensor import SensorDeviceClass
import homeassistant.const as ha

from ocpp.v16.enums import Measurand, UnitOfMeasure

CONF_AUTH_LIST = "authorization_list"
Expand All @@ -22,6 +21,7 @@
CONF_METER_INTERVAL = "meter_interval"
CONF_MODE = ha.CONF_MODE
CONF_MONITORED_VARIABLES = ha.CONF_MONITORED_VARIABLES
CONF_MONITORED_VARIABLES_AUTOCONFIG = "monitored_variables_autoconfig"
CONF_NAME = ha.CONF_NAME
CONF_PASSWORD = ha.CONF_PASSWORD
CONF_PORT = ha.CONF_PORT
Expand Down Expand Up @@ -96,6 +96,7 @@
]
DEFAULT_MEASURAND = Measurand.energy_active_import_register.value
DEFAULT_MONITORED_VARIABLES = ",".join(MEASURANDS)
DEFAULT_MONITORED_VARIABLES_AUTOCONFIG = True
DEFAULT_ENERGY_UNIT = UnitOfMeasure.wh.value
DEFAULT_POWER_UNIT = UnitOfMeasure.w.value
HA_ENERGY_UNIT = UnitOfMeasure.kwh.value
Expand Down
1 change: 0 additions & 1 deletion custom_components/ocpp/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
)
from homeassistant.const import UnitOfPower
from homeassistant.helpers.entity import DeviceInfo

from ocpp.v16.enums import ChargePointStatus

from .api import CentralSystem
Expand Down
46 changes: 23 additions & 23 deletions custom_components/ocpp/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,28 @@
"title": "OCPP-Messwerte",
"description": "Wähle aus welche Messwerte in Home Assistant angezeigt werden sollen.",
"data": {
"Current.Export": "Momentane Stromstärke vom E-Auto",
"Current.Import": "Momentaner Stromstärke zum E-Auto",
"Current.Offered": "Maximale dem E-Auto angebotene Stromstärke",
"Energy.Active.Export.Register": "In das Netz exportierte Wirkenergie",
"Energy.Active.Import.Register": "Aus dem Netz importierte Wirkenergie",
"Energy.Reactive.Export.Register": "In das Netz exportierte Blindenergie",
"Energy.Reactive.Import.Register": "Aus dem Netz importierte Blindenergie",
"Energy.Active.Export.Interval": "Im letzten Intervall in das Netz exportierte Wirkenergie",
"Energy.Active.Import.Interval": "Im letzten Intervall aus dem Netz importierte Wirkenergie",
"Energy.Reactive.Export.Interval": "Im letzten Intervall ins Netz exportierte Blindenergie",
"Energy.Reactive.Import.Interval": "Im letzten Intervall aus dem Netz importierte Blindenergie",
"Frequency": "Netzfrequenz",
"Power.Active.Export": "Von E-Auto exportierte momentane Wirkleistung",
"Power.Active.Import": "Von E-Auto importierte momentane Wirkleistung",
"Power.Factor": "Unmittelbarer Leistungsfaktor des gesamten Energieflusses",
"Power.Offered": "Maximale dem E-Auto angebotene Leistung",
"Power.Reactive.Export": "Von E-Auto exportierte momentane Blindleistung",
"Power.Reactive.Import": "Von E-Auto importierte momentane Blindleistung",
"RPM": "Lüfterdrehzahl in RPM",
"SoC": "Ladezustand des E-Autos in Prozent",
"Temperature": "Temperaturmesswert in der Ladestation",
"Voltage": "Momentane AC-Effektivspannung"
"Current.Export": "Current.Export: Momentane Stromstärke vom E-Auto",
"Current.Import": "Current.Import: Momentaner Stromstärke zum E-Auto",
"Current.Offered": "Current.Offered: Maximale dem E-Auto angebotene Stromstärke",
"Energy.Active.Export.Register": "Energy.Active.Export.Register: In das Netz exportierte Wirkenergie",
"Energy.Active.Import.Register": "Energy.Active.Import.Register: Aus dem Netz importierte Wirkenergie",
"Energy.Reactive.Export.Register": "Energy.Reactive.Export.Register: In das Netz exportierte Blindenergie",
"Energy.Reactive.Import.Register": "Energy.Reactive.Import.Register: Aus dem Netz importierte Blindenergie",
"Energy.Active.Export.Interval": "Energy.Active.Export.Interval: Im letzten Intervall in das Netz exportierte Wirkenergie",
"Energy.Active.Import.Interval": "Energy.Active.Import.Interval: Im letzten Intervall aus dem Netz importierte Wirkenergie",
"Energy.Reactive.Export.Interval": "Energy.Reactive.Export.Interval: Im letzten Intervall ins Netz exportierte Blindenergie",
"Energy.Reactive.Import.Interval": "Energy.Reactive.Import.Interval: Im letzten Intervall aus dem Netz importierte Blindenergie",
"Frequency": "Frequency: Netzfrequenz",
"Power.Active.Export": "Power.Active.Export: Von E-Auto exportierte momentane Wirkleistung",
"Power.Active.Import": "Power.Active.Import: Von E-Auto importierte momentane Wirkleistung",
"Power.Factor": "Power.Factor: Unmittelbarer Leistungsfaktor des gesamten Energieflusses",
"Power.Offered": "Power.Offered: Maximale dem E-Auto angebotene Leistung",
"Power.Reactive.Export": "Power.Reactive.Export: Von E-Auto exportierte momentane Blindleistung",
"Power.Reactive.Import": "Power.Reactive.Import: Von E-Auto importierte momentane Blindleistung",
"RPM": "RPM: Lüfterdrehzahl in RPM",
"SoC": "SoC: Ladezustand des E-Autos in Prozent",
"Temperature": "Temperature: Temperaturmesswert in der Ladestation",
"Voltage": "Voltage: Momentane AC-Effektivspannung"
}
}
},
Expand All @@ -57,4 +57,4 @@
"single_instance_allowed": "Es ist nur eine Instanz erlaubt."
}
}
}
}
49 changes: 25 additions & 24 deletions custom_components/ocpp/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"cpid": "Charge point identity",
"max_current": "Maximum charging current",
"meter_interval": "Charging sample interval (seconds)",
"monitored_variables_autoconfig": "Automatic detection of OCPP Measurands",
"idle_interval": "Charger idle sampling interval (seconds)",
"websocket_close_timeout": "Websocket close timeout (seconds)",
"websocket_ping_tries": "Websocket successive times to try connection before closing",
Expand All @@ -25,30 +26,30 @@
},
"measurands": {
"title": "OCPP Measurands",
"description": "Select which measurand(s) should be shown in Home Assistant.",
"description": "Select which measurand(s) should be used in Home Assistant.",
"data": {
"Current.Export": "Instantaneous current flow from EV",
"Current.Import": "Instantaneous current flow to EV",
"Current.Offered": "Maximum current offered to EV",
"Energy.Active.Export.Register": "Active energy exported to the grid",
"Energy.Active.Import.Register": "Active energy imported from the grid",
"Energy.Reactive.Export.Register": "Reactive energy exported to the grid",
"Energy.Reactive.Import.Register": "Reactive energy imported from the grid",
"Energy.Active.Export.Interval": "Active energy exported to the grid during last interval",
"Energy.Active.Import.Interval": "Active energy imported from the grid during last interval",
"Energy.Reactive.Export.Interval": "Reactive energy exported to the grid during last interval",
"Energy.Reactive.Import.Interval": "Reactive energy imported from the grid during last interval",
"Frequency": "Powerline frequency",
"Power.Active.Export": "Instantaneous active power exported by EV",
"Power.Active.Import": "Instantaneous active power imported by EV",
"Power.Factor": "Instantaneous power factor of total energy flow",
"Power.Offered": "Maximum power offered to EV",
"Power.Reactive.Export": "Instantaneous reactive power exported by EV",
"Power.Reactive.Import": "Instantaneous reactive power imported by EV",
"RPM": "Fan speed in RPM",
"SoC": "State of charge of EV in percentage",
"Temperature": "Temperature reading inside Charge Point",
"Voltage": "Instantaneous AC RMS supply voltage"
"Current.Export": "Current.Export: Instantaneous current flow from EV",
"Current.Import": "Current.Import: Instantaneous current flow to EV",
"Current.Offered": "Current.Offered: Maximum current offered to EV",
"Energy.Active.Export.Register": "Energy.Active.Export.Register: Active energy exported to the grid",
"Energy.Active.Import.Register": "Energy.Active.Import.Register: Active energy imported from the grid",
"Energy.Reactive.Export.Register": "Energy.Reactive.Export.Register: Reactive energy exported to the grid",
"Energy.Reactive.Import.Register": "Energy.Reactive.Import.Register: Reactive energy imported from the grid",
"Energy.Active.Export.Interval": "Energy.Active.Export.Interval: Active energy exported to the grid during last interval",
"Energy.Active.Import.Interval": "Energy.Active.Import.Interval: Active energy imported from the grid during last interval",
"Energy.Reactive.Export.Interval": "Energy.Reactive.Export.Interval: Reactive energy exported to the grid during last interval",
"Energy.Reactive.Import.Interval": "Energy.Reactive.Import.Interval: Reactive energy imported from the grid during last interval",
"Frequency": "Frequency: Powerline frequency",
"Power.Active.Export": "Power.Active.Export: Instantaneous active power exported by EV",
"Power.Active.Import": "Power.Active.Import: Instantaneous active power imported by EV",
"Power.Factor": "Power.Factor: Instantaneous power factor of total energy flow",
"Power.Offered": "Power.Offered: Maximum power offered to EV",
"Power.Reactive.Export": "Power.Reactive.Export: Instantaneous reactive power exported by EV",
"Power.Reactive.Import": "Power.Reactive.Import: Instantaneous reactive power imported by EV",
"RPM": "RPM: Fan speed in RPM",
"SoC": "SoC: State of charge of EV in percentage",
"Temperature": "Temperature: Temperature reading inside Charge Point",
"Voltage": "Voltage: Instantaneous AC RMS supply voltage"
}
}
},
Expand All @@ -60,4 +61,4 @@
"single_instance_allowed": "Only a single instance is allowed."
}
}
}
}
Loading

0 comments on commit b963bd0

Please sign in to comment.