From 46ec510fda46a0d65a5aeb405c54fbd4c1e16ea2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:46:07 +0000 Subject: [PATCH 01/10] Initial plan From 80d3cd820f56565182548b2d4790b21b8b3ab839 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:49:53 +0000 Subject: [PATCH 02/10] Add translation support for error and warning values - Add translation keys for all error codes in strings.json, en.json, and de.json - Modify ErrorSensor and WarningSensor to load and use translations - Add extra_state_attributes with raw error codes for automation use - Change separator from comma to comma+space for better readability Co-authored-by: dkarv <3708591+dkarv@users.noreply.github.com> --- custom_components/bwt_perla/sensors/base.py | 91 +++++++++++++++++-- custom_components/bwt_perla/strings.json | 52 +++++++++++ .../bwt_perla/translations/de.json | 52 +++++++++++ .../bwt_perla/translations/en.json | 52 +++++++++++ 4 files changed, 239 insertions(+), 8 deletions(-) diff --git a/custom_components/bwt_perla/sensors/base.py b/custom_components/bwt_perla/sensors/base.py index 76a496d..21f2c86 100644 --- a/custom_components/bwt_perla/sensors/base.py +++ b/custom_components/bwt_perla/sensors/base.py @@ -17,6 +17,7 @@ from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers import translation from ..const import DOMAIN from ..coordinator import BwtCoordinator @@ -99,14 +100,51 @@ class ErrorSensor(BwtEntity, SensorEntity): def __init__(self, coordinator, device_info, entry_id) -> None: """Initialize the sensor with the common coordinator.""" super().__init__(coordinator, device_info, entry_id, "errors") - values = [x.name for x in self.coordinator.data.errors() if x.is_fatal()] - self._attr_native_value = ",".join(values) + self._translations = None + errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] + self._update_values(errors) + + async def async_added_to_hass(self) -> None: + """When entity is added to hass, load translations.""" + await super().async_added_to_hass() + # Load translations for the current language + self._translations = await translation.async_get_translations( + self.hass, + self.hass.config.language, + "entity_component", + {DOMAIN}, + ) + # Update values with translations now that they're loaded + errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] + self._update_values(errors) + self.async_write_ha_state() + + def _translate_error(self, error_name: str) -> str: + """Translate an error code to the user's language.""" + if self._translations is None: + return error_name + + key = f"component.{DOMAIN}.entity_component._.state.error.{error_name.lower()}" + return self._translations.get(key, error_name) + + def _update_values(self, errors) -> None: + """Update error values with translations.""" + raw_values = [x.name for x in errors] + # Store raw values as extra attributes for automation + self._attr_extra_state_attributes = {"error_codes": raw_values} + + # Translate error names for display + if errors: + translated = [self._translate_error(x.name) for x in errors] + self._attr_native_value = ", ".join(translated) + else: + self._attr_native_value = "" @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - values = [x.name for x in self.coordinator.data.errors() if x.is_fatal()] - self._attr_native_value = ",".join(values) + errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] + self._update_values(errors) self.async_write_ha_state() @@ -118,14 +156,51 @@ class WarningSensor(BwtEntity, SensorEntity): def __init__(self, coordinator, device_info, entry_id) -> None: """Initialize the sensor with the common coordinator.""" super().__init__(coordinator, device_info, entry_id, "warnings") - values = [x.name for x in self.coordinator.data.errors() if not x.is_fatal()] - self._attr_native_value = ",".join(values) + self._translations = None + warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] + self._update_values(warnings) + + async def async_added_to_hass(self) -> None: + """When entity is added to hass, load translations.""" + await super().async_added_to_hass() + # Load translations for the current language + self._translations = await translation.async_get_translations( + self.hass, + self.hass.config.language, + "entity_component", + {DOMAIN}, + ) + # Update values with translations now that they're loaded + warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] + self._update_values(warnings) + self.async_write_ha_state() + + def _translate_error(self, error_name: str) -> str: + """Translate an error code to the user's language.""" + if self._translations is None: + return error_name + + key = f"component.{DOMAIN}.entity_component._.state.error.{error_name.lower()}" + return self._translations.get(key, error_name) + + def _update_values(self, warnings) -> None: + """Update warning values with translations.""" + raw_values = [x.name for x in warnings] + # Store raw values as extra attributes for automation + self._attr_extra_state_attributes = {"warning_codes": raw_values} + + # Translate warning names for display + if warnings: + translated = [self._translate_error(x.name) for x in warnings] + self._attr_native_value = ", ".join(translated) + else: + self._attr_native_value = "" @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - values = [x.name for x in self.coordinator.data.errors() if not x.is_fatal()] - self._attr_native_value = ",".join(values) + warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] + self._update_values(warnings) self.async_write_ha_state() diff --git a/custom_components/bwt_perla/strings.json b/custom_components/bwt_perla/strings.json index f4444b2..1521eb9 100644 --- a/custom_components/bwt_perla/strings.json +++ b/custom_components/bwt_perla/strings.json @@ -101,5 +101,57 @@ "name": "Total dosing means used" } } + }, + "entity_component": { + "_": { + "state": { + "error": { + "unknown": "Unknown error", + "offline_motor_1": "Motor 1 offline", + "offline_motor_2": "Motor 2 offline", + "offline_motor_blend": "Blend motor offline", + "regenerativ_20": "Regeneration salt level at 20%", + "overcurrent_motor_1": "Overcurrent motor 1", + "overcurrent_motor_2": "Overcurrent motor 2", + "overcurrent_motor_3": "Overcurrent motor 3", + "overcurrent_valve": "Overcurrent valve", + "stop_volume": "Stop volume", + "stop_sensor": "Stop sensor", + "constant_flow": "Constant flow", + "low_pressure": "Low pressure", + "piston_position": "Piston position error", + "electronic": "Electronic error", + "insufficient_regenerativ": "Insufficient regeneration salt", + "stop_wireless_sensor": "Stop wireless sensor", + "regenerativ_0": "Regeneration salt empty", + "maintenance_customer": "Customer maintenance required", + "inspection_customer": "Customer inspection required", + "maintenance_service": "Service maintenance required", + "minerals_low": "Minerals low", + "minerals_0": "Minerals empty", + "overcurrent_valve_1": "Overcurrent valve 1", + "overcurrent_valve_2": "Overcurrent valve 2", + "overcurrent_dosing": "Overcurrent dosing", + "overcurrent_valve_ball": "Overcurrent ball valve", + "meter_not_counting": "Water meter not counting", + "regeneration_drain": "Regeneration drain issue", + "init_pcb_0": "PCB initialization error 0", + "init_pcb_1": "PCB initialization error 1", + "position_motor_1": "Motor 1 position error", + "position_motor_2": "Motor 2 position error", + "conductivity_high": "Conductivity too high", + "conductivity_limit_1": "Conductivity limit 1 exceeded", + "conductivity_limit_2": "Conductivity limit 2 exceeded", + "conductivity_limit_water": "Water conductivity limit exceeded", + "no_function": "No function", + "temperature_disconnected": "Temperature sensor disconnected", + "temperature_high": "Temperature too high", + "offline_valve_ball": "Ball valve offline", + "external_filter_change": "External filter change required", + "brine_unsaturated": "Brine unsaturated", + "dosing_fault": "Dosing fault" + } + } + } } } \ No newline at end of file diff --git a/custom_components/bwt_perla/translations/de.json b/custom_components/bwt_perla/translations/de.json index c61bcf4..0dc019c 100644 --- a/custom_components/bwt_perla/translations/de.json +++ b/custom_components/bwt_perla/translations/de.json @@ -101,5 +101,57 @@ "name": "Verbrauchte Menge Dosiermittel seit Inbetriebnahme" } } + }, + "entity_component": { + "_": { + "state": { + "error": { + "unknown": "Unbekannter Fehler", + "offline_motor_1": "Motor 1 offline", + "offline_motor_2": "Motor 2 offline", + "offline_motor_blend": "Mischmotor offline", + "regenerativ_20": "Regeneriersalz-Stand bei 20%", + "overcurrent_motor_1": "Überstrom Motor 1", + "overcurrent_motor_2": "Überstrom Motor 2", + "overcurrent_motor_3": "Überstrom Motor 3", + "overcurrent_valve": "Überstrom Ventil", + "stop_volume": "Volumen-Stopp", + "stop_sensor": "Sensor-Stopp", + "constant_flow": "Konstanter Durchfluss", + "low_pressure": "Niedriger Druck", + "piston_position": "Kolbenpositionsfehler", + "electronic": "Elektronikfehler", + "insufficient_regenerativ": "Unzureichendes Regeneriersalz", + "stop_wireless_sensor": "Funk-Sensor-Stopp", + "regenerativ_0": "Regeneriersalz leer", + "maintenance_customer": "Kundenwartung erforderlich", + "inspection_customer": "Kundeninspektion erforderlich", + "maintenance_service": "Servicewartung erforderlich", + "minerals_low": "Mineralien niedrig", + "minerals_0": "Mineralien leer", + "overcurrent_valve_1": "Überstrom Ventil 1", + "overcurrent_valve_2": "Überstrom Ventil 2", + "overcurrent_dosing": "Überstrom Dosierung", + "overcurrent_valve_ball": "Überstrom Kugelventil", + "meter_not_counting": "Wasserzähler zählt nicht", + "regeneration_drain": "Regenerationsabfluss-Problem", + "init_pcb_0": "Leiterplatten-Initialisierungsfehler 0", + "init_pcb_1": "Leiterplatten-Initialisierungsfehler 1", + "position_motor_1": "Positionsfehler Motor 1", + "position_motor_2": "Positionsfehler Motor 2", + "conductivity_high": "Leitfähigkeit zu hoch", + "conductivity_limit_1": "Leitfähigkeitsgrenze 1 überschritten", + "conductivity_limit_2": "Leitfähigkeitsgrenze 2 überschritten", + "conductivity_limit_water": "Wasser-Leitfähigkeitsgrenze überschritten", + "no_function": "Keine Funktion", + "temperature_disconnected": "Temperatursensor getrennt", + "temperature_high": "Temperatur zu hoch", + "offline_valve_ball": "Kugelventil offline", + "external_filter_change": "Externer Filterwechsel erforderlich", + "brine_unsaturated": "Sole ungesättigt", + "dosing_fault": "Dosierfehler" + } + } + } } } \ No newline at end of file diff --git a/custom_components/bwt_perla/translations/en.json b/custom_components/bwt_perla/translations/en.json index 3c2c5f6..6fe4634 100644 --- a/custom_components/bwt_perla/translations/en.json +++ b/custom_components/bwt_perla/translations/en.json @@ -101,5 +101,57 @@ "name": "Total dosing means used" } } + }, + "entity_component": { + "_": { + "state": { + "error": { + "unknown": "Unknown error", + "offline_motor_1": "Motor 1 offline", + "offline_motor_2": "Motor 2 offline", + "offline_motor_blend": "Blend motor offline", + "regenerativ_20": "Regeneration salt level at 20%", + "overcurrent_motor_1": "Overcurrent motor 1", + "overcurrent_motor_2": "Overcurrent motor 2", + "overcurrent_motor_3": "Overcurrent motor 3", + "overcurrent_valve": "Overcurrent valve", + "stop_volume": "Stop volume", + "stop_sensor": "Stop sensor", + "constant_flow": "Constant flow", + "low_pressure": "Low pressure", + "piston_position": "Piston position error", + "electronic": "Electronic error", + "insufficient_regenerativ": "Insufficient regeneration salt", + "stop_wireless_sensor": "Stop wireless sensor", + "regenerativ_0": "Regeneration salt empty", + "maintenance_customer": "Customer maintenance required", + "inspection_customer": "Customer inspection required", + "maintenance_service": "Service maintenance required", + "minerals_low": "Minerals low", + "minerals_0": "Minerals empty", + "overcurrent_valve_1": "Overcurrent valve 1", + "overcurrent_valve_2": "Overcurrent valve 2", + "overcurrent_dosing": "Overcurrent dosing", + "overcurrent_valve_ball": "Overcurrent ball valve", + "meter_not_counting": "Water meter not counting", + "regeneration_drain": "Regeneration drain issue", + "init_pcb_0": "PCB initialization error 0", + "init_pcb_1": "PCB initialization error 1", + "position_motor_1": "Motor 1 position error", + "position_motor_2": "Motor 2 position error", + "conductivity_high": "Conductivity too high", + "conductivity_limit_1": "Conductivity limit 1 exceeded", + "conductivity_limit_2": "Conductivity limit 2 exceeded", + "conductivity_limit_water": "Water conductivity limit exceeded", + "no_function": "No function", + "temperature_disconnected": "Temperature sensor disconnected", + "temperature_high": "Temperature too high", + "offline_valve_ball": "Ball valve offline", + "external_filter_change": "External filter change required", + "brine_unsaturated": "Brine unsaturated", + "dosing_fault": "Dosing fault" + } + } + } } } \ No newline at end of file From d9addaeffa4145ea44f618f3f0f46f022d642a2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:50:30 +0000 Subject: [PATCH 03/10] Update README with error/warning translation documentation Add FAQ section explaining how to use error codes in automations Update entity table to clarify translated error messages and attribute access Co-authored-by: dkarv <3708591+dkarv@users.noreply.github.com> --- README.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b536de1..e354b5c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ UK *BWT Perla Silk*: | Entity Id(s) | Information | | ------------- | ------------- | | total_output | Increasing value of the blended water = the total water consumed. Use this as water source on the energy dashboard. | -| errors, warnings | The fatal errors and non-fatal warnings. Comma separated list or empty if no value present. [List of values](https://github.com/dkarv/bwt_api/blob/main/src/bwt_api/error.py). | +| errors, warnings | The fatal errors and non-fatal warnings. Displays a comma-separated list of translated error/warning messages in your language (English and German supported). Raw error codes are available in the entity attributes (`error_codes` or `warning_codes`) for use in automations. Empty if no errors/warnings present. [List of error codes](https://github.com/dkarv/bwt_api/blob/main/src/bwt_api/error.py). | | state | State of the device. Can be OK, WARNING, ERROR | | holiday_mode | If the holiday mode is active (true) or not (false) | | holiday_mode_start | Undefined or a timestamp if the holiday mode is set to start in the future | @@ -46,6 +46,27 @@ UK *BWT Perla Silk*: ### FAQ +#### How do I use error and warning codes in automations? + +Error and warning entities display translated, human-readable messages. For automations, you can access the raw error codes through entity attributes: + +```yaml +automation: + - alias: "Alert on specific BWT error" + trigger: + - platform: state + entity_id: sensor.bwt_perla_errors + condition: + - condition: template + value_template: "{{ 'OFFLINE_MOTOR_1' in state_attr('sensor.bwt_perla_errors', 'error_codes') }}" + action: + - service: notify.mobile_app + data: + message: "BWT Perla: Motor 1 is offline!" +``` + +Similarly for warnings, use `state_attr('sensor.bwt_perla_warnings', 'warning_codes')`. + #### How can I get the firmware update? This is only relevant for *Perla One* and *Perla Duplex* devices. *Silk* devices in the UK do have different firmware versions. With the latest update, they are also supported with a limited set of entities. From 05793b4efb44bdcc07c699a5a528d8390617b1b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:52:11 +0000 Subject: [PATCH 04/10] Refactor: Extract translation method to base class - Move _translate_error method from ErrorSensor and WarningSensor to BwtEntity - Remove code duplication - Remove trailing whitespace for better code style Co-authored-by: dkarv <3708591+dkarv@users.noreply.github.com> --- custom_components/bwt_perla/sensors/base.py | 31 ++++++++------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/custom_components/bwt_perla/sensors/base.py b/custom_components/bwt_perla/sensors/base.py index 21f2c86..eed5b5d 100644 --- a/custom_components/bwt_perla/sensors/base.py +++ b/custom_components/bwt_perla/sensors/base.py @@ -47,6 +47,15 @@ def __init__( self._attr_has_entity_name = True self.entity_id = f"sensor.{DOMAIN}_{key}" self._attr_unique_id = entry_id + "_" + key + self._translations = None + + def _translate_error(self, error_name: str) -> str: + """Translate an error code to the user's language.""" + if self._translations is None: + return error_name + + key = f"component.{DOMAIN}.entity_component._.state.error.{error_name.lower()}" + return self._translations.get(key, error_name) class TotalOutputSensor(BwtEntity, SensorEntity): @@ -100,7 +109,6 @@ class ErrorSensor(BwtEntity, SensorEntity): def __init__(self, coordinator, device_info, entry_id) -> None: """Initialize the sensor with the common coordinator.""" super().__init__(coordinator, device_info, entry_id, "errors") - self._translations = None errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] self._update_values(errors) @@ -119,20 +127,12 @@ async def async_added_to_hass(self) -> None: self._update_values(errors) self.async_write_ha_state() - def _translate_error(self, error_name: str) -> str: - """Translate an error code to the user's language.""" - if self._translations is None: - return error_name - - key = f"component.{DOMAIN}.entity_component._.state.error.{error_name.lower()}" - return self._translations.get(key, error_name) - def _update_values(self, errors) -> None: """Update error values with translations.""" raw_values = [x.name for x in errors] # Store raw values as extra attributes for automation self._attr_extra_state_attributes = {"error_codes": raw_values} - + # Translate error names for display if errors: translated = [self._translate_error(x.name) for x in errors] @@ -156,7 +156,6 @@ class WarningSensor(BwtEntity, SensorEntity): def __init__(self, coordinator, device_info, entry_id) -> None: """Initialize the sensor with the common coordinator.""" super().__init__(coordinator, device_info, entry_id, "warnings") - self._translations = None warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] self._update_values(warnings) @@ -175,20 +174,12 @@ async def async_added_to_hass(self) -> None: self._update_values(warnings) self.async_write_ha_state() - def _translate_error(self, error_name: str) -> str: - """Translate an error code to the user's language.""" - if self._translations is None: - return error_name - - key = f"component.{DOMAIN}.entity_component._.state.error.{error_name.lower()}" - return self._translations.get(key, error_name) - def _update_values(self, warnings) -> None: """Update warning values with translations.""" raw_values = [x.name for x in warnings] # Store raw values as extra attributes for automation self._attr_extra_state_attributes = {"warning_codes": raw_values} - + # Translate warning names for display if warnings: translated = [self._translate_error(x.name) for x in warnings] From 71c031b6e64b98174ad80cdc17df95baebb5ae02 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:53:49 +0000 Subject: [PATCH 05/10] Refactor: Create TranslatableErrorMixin to eliminate duplication - Create TranslatableErrorMixin class for shared translation logic - Rename _translate_error to _translate_code to reflect dual purpose - Move async_added_to_hass translation loading to mixin - Update ErrorSensor and WarningSensor to use the mixin - Eliminates all code duplication between error and warning sensors Co-authored-by: dkarv <3708591+dkarv@users.noreply.github.com> --- custom_components/bwt_perla/sensors/base.py | 51 +++++++++++---------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/custom_components/bwt_perla/sensors/base.py b/custom_components/bwt_perla/sensors/base.py index eed5b5d..aa1c7e0 100644 --- a/custom_components/bwt_perla/sensors/base.py +++ b/custom_components/bwt_perla/sensors/base.py @@ -47,15 +47,32 @@ def __init__( self._attr_has_entity_name = True self.entity_id = f"sensor.{DOMAIN}_{key}" self._attr_unique_id = entry_id + "_" + key - self._translations = None - def _translate_error(self, error_name: str) -> str: - """Translate an error code to the user's language.""" + +class TranslatableErrorMixin: + """Mixin for entities that need to translate error codes.""" + + _translations = None + + async def async_added_to_hass(self) -> None: + """When entity is added to hass, load translations.""" + await super().async_added_to_hass() + # Load translations for the current language + self._translations = await translation.async_get_translations( + self.hass, + self.hass.config.language, + "entity_component", + {DOMAIN}, + ) + + def _translate_code(self, code_name: str) -> str: + """Translate an error/warning code to the user's language.""" if self._translations is None: - return error_name + return code_name + + key = f"component.{DOMAIN}.entity_component._.state.error.{code_name.lower()}" + return self._translations.get(key, code_name) - key = f"component.{DOMAIN}.entity_component._.state.error.{error_name.lower()}" - return self._translations.get(key, error_name) class TotalOutputSensor(BwtEntity, SensorEntity): @@ -101,7 +118,7 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() -class ErrorSensor(BwtEntity, SensorEntity): +class ErrorSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): """Errors reported by the device.""" _attr_icon = _ERROR @@ -115,13 +132,6 @@ def __init__(self, coordinator, device_info, entry_id) -> None: async def async_added_to_hass(self) -> None: """When entity is added to hass, load translations.""" await super().async_added_to_hass() - # Load translations for the current language - self._translations = await translation.async_get_translations( - self.hass, - self.hass.config.language, - "entity_component", - {DOMAIN}, - ) # Update values with translations now that they're loaded errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] self._update_values(errors) @@ -135,7 +145,7 @@ def _update_values(self, errors) -> None: # Translate error names for display if errors: - translated = [self._translate_error(x.name) for x in errors] + translated = [self._translate_code(x.name) for x in errors] self._attr_native_value = ", ".join(translated) else: self._attr_native_value = "" @@ -148,7 +158,7 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() -class WarningSensor(BwtEntity, SensorEntity): +class WarningSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): """Warnings reported by the device.""" _attr_icon = _WARNING @@ -162,13 +172,6 @@ def __init__(self, coordinator, device_info, entry_id) -> None: async def async_added_to_hass(self) -> None: """When entity is added to hass, load translations.""" await super().async_added_to_hass() - # Load translations for the current language - self._translations = await translation.async_get_translations( - self.hass, - self.hass.config.language, - "entity_component", - {DOMAIN}, - ) # Update values with translations now that they're loaded warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] self._update_values(warnings) @@ -182,7 +185,7 @@ def _update_values(self, warnings) -> None: # Translate warning names for display if warnings: - translated = [self._translate_error(x.name) for x in warnings] + translated = [self._translate_code(x.name) for x in warnings] self._attr_native_value = ", ".join(translated) else: self._attr_native_value = "" From fc8d5e3c676b60db4bbec05b11b3de17cbb0be72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:55:07 +0000 Subject: [PATCH 06/10] Polish: Extract helper methods and remove extra blank line - Add _get_errors() helper method to ErrorSensor - Add _get_warnings() helper method to WarningSensor - Remove extra blank line between classes - Further reduce code duplication Co-authored-by: dkarv <3708591+dkarv@users.noreply.github.com> --- custom_components/bwt_perla/sensors/base.py | 27 +++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/custom_components/bwt_perla/sensors/base.py b/custom_components/bwt_perla/sensors/base.py index aa1c7e0..d9455a1 100644 --- a/custom_components/bwt_perla/sensors/base.py +++ b/custom_components/bwt_perla/sensors/base.py @@ -74,7 +74,6 @@ def _translate_code(self, code_name: str) -> str: return self._translations.get(key, code_name) - class TotalOutputSensor(BwtEntity, SensorEntity): """Total water [liter] that passed through the output.""" @@ -126,15 +125,17 @@ class ErrorSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): def __init__(self, coordinator, device_info, entry_id) -> None: """Initialize the sensor with the common coordinator.""" super().__init__(coordinator, device_info, entry_id, "errors") - errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] - self._update_values(errors) + self._update_values(self._get_errors()) + + def _get_errors(self): + """Get the current list of fatal errors.""" + return [x for x in self.coordinator.data.errors() if x.is_fatal()] async def async_added_to_hass(self) -> None: """When entity is added to hass, load translations.""" await super().async_added_to_hass() # Update values with translations now that they're loaded - errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] - self._update_values(errors) + self._update_values(self._get_errors()) self.async_write_ha_state() def _update_values(self, errors) -> None: @@ -153,8 +154,7 @@ def _update_values(self, errors) -> None: @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - errors = [x for x in self.coordinator.data.errors() if x.is_fatal()] - self._update_values(errors) + self._update_values(self._get_errors()) self.async_write_ha_state() @@ -166,15 +166,17 @@ class WarningSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): def __init__(self, coordinator, device_info, entry_id) -> None: """Initialize the sensor with the common coordinator.""" super().__init__(coordinator, device_info, entry_id, "warnings") - warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] - self._update_values(warnings) + self._update_values(self._get_warnings()) + + def _get_warnings(self): + """Get the current list of non-fatal warnings.""" + return [x for x in self.coordinator.data.errors() if not x.is_fatal()] async def async_added_to_hass(self) -> None: """When entity is added to hass, load translations.""" await super().async_added_to_hass() # Update values with translations now that they're loaded - warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] - self._update_values(warnings) + self._update_values(self._get_warnings()) self.async_write_ha_state() def _update_values(self, warnings) -> None: @@ -193,8 +195,7 @@ def _update_values(self, warnings) -> None: @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - warnings = [x for x in self.coordinator.data.errors() if not x.is_fatal()] - self._update_values(warnings) + self._update_values(self._get_warnings()) self.async_write_ha_state() From f88814cdbd162615ce86dfa816888f70e92f1b33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:55:56 +0000 Subject: [PATCH 07/10] Docs: Add documentation to TranslatableErrorMixin Add comprehensive docstring to TranslatableErrorMixin class explaining its purpose and the _translations attribute lifecycle Co-authored-by: dkarv <3708591+dkarv@users.noreply.github.com> --- custom_components/bwt_perla/sensors/base.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/custom_components/bwt_perla/sensors/base.py b/custom_components/bwt_perla/sensors/base.py index d9455a1..f6c7b77 100644 --- a/custom_components/bwt_perla/sensors/base.py +++ b/custom_components/bwt_perla/sensors/base.py @@ -50,7 +50,16 @@ def __init__( class TranslatableErrorMixin: - """Mixin for entities that need to translate error codes.""" + """Mixin for entities that need to translate error codes. + + This mixin provides translation functionality for entities that display + multiple error/warning codes. It loads translations when the entity is + added to Home Assistant and provides a method to translate individual codes. + + Attributes: + _translations: Dictionary of translations loaded from language files. + Initialized as None and populated in async_added_to_hass. + """ _translations = None From 6d7bd9f318197800accc126bf70a99cfb614bcc5 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 14 Feb 2026 00:07:58 +0100 Subject: [PATCH 08/10] adjust error/warning description --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index e354b5c..1279f7f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ UK *BWT Perla Silk*: | Entity Id(s) | Information | | ------------- | ------------- | | total_output | Increasing value of the blended water = the total water consumed. Use this as water source on the energy dashboard. | -| errors, warnings | The fatal errors and non-fatal warnings. Displays a comma-separated list of translated error/warning messages in your language (English and German supported). Raw error codes are available in the entity attributes (`error_codes` or `warning_codes`) for use in automations. Empty if no errors/warnings present. [List of error codes](https://github.com/dkarv/bwt_api/blob/main/src/bwt_api/error.py). | +| errors, warnings | The fatal errors and non-fatal warnings. Displays a comma-separated list of translated error/warning messages. Raw error codes are available in the entity attributes (`error_codes` or `warning_codes`) for use in automations. Empty if no errors/warnings present. [List of error codes](https://github.com/dkarv/bwt_api/blob/main/src/bwt_api/error.py). | | state | State of the device. Can be OK, WARNING, ERROR | | holiday_mode | If the holiday mode is active (true) or not (false) | | holiday_mode_start | Undefined or a timestamp if the holiday mode is set to start in the future | @@ -46,27 +46,6 @@ UK *BWT Perla Silk*: ### FAQ -#### How do I use error and warning codes in automations? - -Error and warning entities display translated, human-readable messages. For automations, you can access the raw error codes through entity attributes: - -```yaml -automation: - - alias: "Alert on specific BWT error" - trigger: - - platform: state - entity_id: sensor.bwt_perla_errors - condition: - - condition: template - value_template: "{{ 'OFFLINE_MOTOR_1' in state_attr('sensor.bwt_perla_errors', 'error_codes') }}" - action: - - service: notify.mobile_app - data: - message: "BWT Perla: Motor 1 is offline!" -``` - -Similarly for warnings, use `state_attr('sensor.bwt_perla_warnings', 'warning_codes')`. - #### How can I get the firmware update? This is only relevant for *Perla One* and *Perla Duplex* devices. *Silk* devices in the UK do have different firmware versions. With the latest update, they are also supported with a limited set of entities. @@ -95,3 +74,24 @@ There are three different volume values, related to how the BWT operates interna * _blended water_: By mixing _incoming water_ and _fully desalinated water_ at a given ratio, the requested hardness is produced. A small example: Target of _blended water_ is 4dH, incoming 20dH. The BWT mixes now 20% of incoming water with 80% fully desalinated water: 0.2 * 20dH + 0.8 * 0dH = 4dH. + +#### How do I use error and warning codes in automations? + +Error and warning entities display translated, human-readable messages. For automations, you can access the raw error codes through entity attributes: + +```yaml +automation: + - alias: "Alert on specific BWT error" + trigger: + - platform: state + entity_id: sensor.bwt_perla_errors + condition: + - condition: template + value_template: "{{ 'OFFLINE_MOTOR_1' in state_attr('sensor.bwt_perla_errors', 'error_codes') }}" + action: + - service: notify.mobile_app + data: + message: "BWT Perla: Motor 1 is offline!" +``` + +Similarly for warnings, use `state_attr('sensor.bwt_perla_warnings', 'warning_codes')`. From 24269e3f758fb839f3ed4ab935b8564b5e3b3d30 Mon Sep 17 00:00:00 2001 From: dkarv Date: Sun, 15 Feb 2026 09:42:19 +0000 Subject: [PATCH 09/10] adjust error/warning logic and translations --- custom_components/bwt_perla/sensors/base.py | 19 ++- custom_components/bwt_perla/strings.json | 112 +++++++++--------- .../bwt_perla/translations/de.json | 8 +- .../bwt_perla/translations/en.json | 20 ++-- custom_components/bwt_perla/util.py | 11 ++ dev/configuration.yaml | 8 -- 6 files changed, 94 insertions(+), 84 deletions(-) create mode 100644 custom_components/bwt_perla/util.py diff --git a/custom_components/bwt_perla/sensors/base.py b/custom_components/bwt_perla/sensors/base.py index f6c7b77..d42e151 100644 --- a/custom_components/bwt_perla/sensors/base.py +++ b/custom_components/bwt_perla/sensors/base.py @@ -1,6 +1,8 @@ from datetime import datetime +import logging + from bwt_api.api import treated_to_blended from bwt_api.data import BwtStatus @@ -19,9 +21,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers import translation +from ..util import truncate_value + from ..const import DOMAIN from ..coordinator import BwtCoordinator +_LOGGER = logging.getLogger(__name__) + _FAUCET = "mdi:faucet" _WATER = "mdi:water" _WARNING = "mdi:alert-circle" @@ -156,7 +162,9 @@ def _update_values(self, errors) -> None: # Translate error names for display if errors: translated = [self._translate_code(x.name) for x in errors] - self._attr_native_value = ", ".join(translated) + # Join translated parts and ensure it does not exceed 255 chars + joined = ", ".join(translated) + self._attr_native_value = truncate_value(joined, 255) else: self._attr_native_value = "" @@ -195,11 +203,10 @@ def _update_values(self, warnings) -> None: self._attr_extra_state_attributes = {"warning_codes": raw_values} # Translate warning names for display - if warnings: - translated = [self._translate_code(x.name) for x in warnings] - self._attr_native_value = ", ".join(translated) - else: - self._attr_native_value = "" + translated = [self._translate_code(x.name) for x in warnings] + # Join translated parts and ensure it does not exceed 255 chars + joined = ", ".join(translated) + self._attr_native_value = truncate_value(joined, 255) @callback def _handle_coordinator_update(self) -> None: diff --git a/custom_components/bwt_perla/strings.json b/custom_components/bwt_perla/strings.json index 1521eb9..67b5c59 100644 --- a/custom_components/bwt_perla/strings.json +++ b/custom_components/bwt_perla/strings.json @@ -1,90 +1,90 @@ { "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_code_or_api_disabled": "Invalid code or local API not enabled. Please check: 1) Device firmware is version 2.02xx or later, 2) Local API is enabled in device Settings > General > Connection, 3) Login code is correct (sent via email during registration)", + "unknown": "Unexpected error" + }, "step": { "user": { "data": { - "host": "[%key:common::config_flow::data::host%]", - "code": "User-Code" + "code": "User-Code", + "host": "Host" } } - }, - "error": { - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_code_or_api_disabled": "Invalid code or local API not enabled. Please check: 1) Device firmware is version 2.02xx or later, 2) Local API is enabled in device Settings > General > Connection, 3) Login code is correct (sent via email during registration)", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "entity": { "sensor": { - "regenerativ_mass": { - "name": "Total regeneration salt ever used" - }, - "regenerativ_days": { - "name": "Days left of regeneration salt" - }, - "regenerativ_level": { - "name": "Precentage of regeneration salt" + "capacity_1": { + "name": "Remaining capacity of column 1" }, - "state": { - "name": "State of the machine" + "capacity_2": { + "name": "Remaining capacity of column 1" }, - "errors": { - "name": "Active errors" + "counter_regeneration_1": { + "name": "Regeneration counter column 1" }, - "warnings": { - "name": "Active warnings" + "counter_regeneration_2": { + "name": "Regeneration counter column 2" }, - "technician_service": { - "name": "Last service by technician" + "current_flow": { + "name": "Current flow" }, "customer_service": { "name": "Last service by customer" }, - "hardness_out": { - "name": "Outgoing water hardness" + "day_output": { + "name": "Output of current day" + }, + "errors": { + "name": "Active errors" }, "hardness_in": { "name": "Incoming water hardness" }, - "total_output": { - "name": "Total water consumption" + "hardness_out": { + "name": "Outgoing water hardness" }, "holiday_mode_start": { "name": "Future start of holiday mode if available" }, - "counter_regeneration_1": { - "name": "Regeneration counter column 1" - }, - "counter_regeneration_2": { - "name": "Regeneration counter column 2" - }, "last_regeneration_1": { "name": "Last regeneration column 1" }, "last_regeneration_2": { "name": "Last regeneration column 2" }, - "capacity_1": { - "name": "Remaining capacity of column 1" + "month_output": { + "name": "Output of current month" }, - "capacity_2": { - "name": "Remaining capacity of column 2" + "regenerativ_days": { + "name": "Days left of regeneration salt" }, - "day_output": { - "name": "Output of current day" + "regenerativ_level": { + "name": "Percentage of regeneration salt" }, - "month_output": { - "name": "Output of current month" + "regenerativ_mass": { + "name": "Total regeneration salt ever used" + }, + "state": { + "name": "State of the machine" + }, + "technician_service": { + "name": "Last service by technician" + }, + "total_output": { + "name": "Total water consumption" + }, + "warnings": { + "name": "Active warnings" }, "year_output": { "name": "Output of current year" }, - "current_flow": { - "name": "Current flow" - }, "holiday_mode": { "name": "Holiday mode active" }, @@ -110,7 +110,7 @@ "offline_motor_1": "Motor 1 offline", "offline_motor_2": "Motor 2 offline", "offline_motor_blend": "Blend motor offline", - "regenerativ_20": "Regeneration salt level at 20%", + "regenerativ_20": "Regeneration salt level < 20%", "overcurrent_motor_1": "Overcurrent motor 1", "overcurrent_motor_2": "Overcurrent motor 2", "overcurrent_motor_3": "Overcurrent motor 3", @@ -119,14 +119,14 @@ "stop_sensor": "Stop sensor", "constant_flow": "Constant flow", "low_pressure": "Low pressure", - "piston_position": "Piston position error", - "electronic": "Electronic error", + "piston_position": "Piston position", + "electronic": "Electronic", "insufficient_regenerativ": "Insufficient regeneration salt", "stop_wireless_sensor": "Stop wireless sensor", "regenerativ_0": "Regeneration salt empty", - "maintenance_customer": "Customer maintenance required", + "maintenance_customer": "Routine maintenance due", "inspection_customer": "Customer inspection required", - "maintenance_service": "Service maintenance required", + "maintenance_service": "Technician maintenance due", "minerals_low": "Minerals low", "minerals_0": "Minerals empty", "overcurrent_valve_1": "Overcurrent valve 1", @@ -135,10 +135,10 @@ "overcurrent_valve_ball": "Overcurrent ball valve", "meter_not_counting": "Water meter not counting", "regeneration_drain": "Regeneration drain issue", - "init_pcb_0": "PCB initialization error 0", - "init_pcb_1": "PCB initialization error 1", - "position_motor_1": "Motor 1 position error", - "position_motor_2": "Motor 2 position error", + "init_pcb_0": "PCB initialization 0", + "init_pcb_1": "PCB initialization 1", + "position_motor_1": "Motor 1 position", + "position_motor_2": "Motor 2 position", "conductivity_high": "Conductivity too high", "conductivity_limit_1": "Conductivity limit 1 exceeded", "conductivity_limit_2": "Conductivity limit 2 exceeded", diff --git a/custom_components/bwt_perla/translations/de.json b/custom_components/bwt_perla/translations/de.json index 0dc019c..aca1f22 100644 --- a/custom_components/bwt_perla/translations/de.json +++ b/custom_components/bwt_perla/translations/de.json @@ -110,7 +110,7 @@ "offline_motor_1": "Motor 1 offline", "offline_motor_2": "Motor 2 offline", "offline_motor_blend": "Mischmotor offline", - "regenerativ_20": "Regeneriersalz-Stand bei 20%", + "regenerativ_20": "Regeneriersalz-Stand < 20%", "overcurrent_motor_1": "Überstrom Motor 1", "overcurrent_motor_2": "Überstrom Motor 2", "overcurrent_motor_3": "Überstrom Motor 3", @@ -126,7 +126,7 @@ "regenerativ_0": "Regeneriersalz leer", "maintenance_customer": "Kundenwartung erforderlich", "inspection_customer": "Kundeninspektion erforderlich", - "maintenance_service": "Servicewartung erforderlich", + "maintenance_service": "Technikerwartung erforderlich", "minerals_low": "Mineralien niedrig", "minerals_0": "Mineralien leer", "overcurrent_valve_1": "Überstrom Ventil 1", @@ -135,8 +135,8 @@ "overcurrent_valve_ball": "Überstrom Kugelventil", "meter_not_counting": "Wasserzähler zählt nicht", "regeneration_drain": "Regenerationsabfluss-Problem", - "init_pcb_0": "Leiterplatten-Initialisierungsfehler 0", - "init_pcb_1": "Leiterplatten-Initialisierungsfehler 1", + "init_pcb_0": "PCB-Initialisierungsfehler 0", + "init_pcb_1": "PCB-Initialisierungsfehler 1", "position_motor_1": "Positionsfehler Motor 1", "position_motor_2": "Positionsfehler Motor 2", "conductivity_high": "Leitfähigkeit zu hoch", diff --git a/custom_components/bwt_perla/translations/en.json b/custom_components/bwt_perla/translations/en.json index 6fe4634..67b5c59 100644 --- a/custom_components/bwt_perla/translations/en.json +++ b/custom_components/bwt_perla/translations/en.json @@ -65,7 +65,7 @@ "name": "Days left of regeneration salt" }, "regenerativ_level": { - "name": "Precentage of regeneration salt" + "name": "Percentage of regeneration salt" }, "regenerativ_mass": { "name": "Total regeneration salt ever used" @@ -110,7 +110,7 @@ "offline_motor_1": "Motor 1 offline", "offline_motor_2": "Motor 2 offline", "offline_motor_blend": "Blend motor offline", - "regenerativ_20": "Regeneration salt level at 20%", + "regenerativ_20": "Regeneration salt level < 20%", "overcurrent_motor_1": "Overcurrent motor 1", "overcurrent_motor_2": "Overcurrent motor 2", "overcurrent_motor_3": "Overcurrent motor 3", @@ -119,14 +119,14 @@ "stop_sensor": "Stop sensor", "constant_flow": "Constant flow", "low_pressure": "Low pressure", - "piston_position": "Piston position error", - "electronic": "Electronic error", + "piston_position": "Piston position", + "electronic": "Electronic", "insufficient_regenerativ": "Insufficient regeneration salt", "stop_wireless_sensor": "Stop wireless sensor", "regenerativ_0": "Regeneration salt empty", - "maintenance_customer": "Customer maintenance required", + "maintenance_customer": "Routine maintenance due", "inspection_customer": "Customer inspection required", - "maintenance_service": "Service maintenance required", + "maintenance_service": "Technician maintenance due", "minerals_low": "Minerals low", "minerals_0": "Minerals empty", "overcurrent_valve_1": "Overcurrent valve 1", @@ -135,10 +135,10 @@ "overcurrent_valve_ball": "Overcurrent ball valve", "meter_not_counting": "Water meter not counting", "regeneration_drain": "Regeneration drain issue", - "init_pcb_0": "PCB initialization error 0", - "init_pcb_1": "PCB initialization error 1", - "position_motor_1": "Motor 1 position error", - "position_motor_2": "Motor 2 position error", + "init_pcb_0": "PCB initialization 0", + "init_pcb_1": "PCB initialization 1", + "position_motor_1": "Motor 1 position", + "position_motor_2": "Motor 2 position", "conductivity_high": "Conductivity too high", "conductivity_limit_1": "Conductivity limit 1 exceeded", "conductivity_limit_2": "Conductivity limit 2 exceeded", diff --git a/custom_components/bwt_perla/util.py b/custom_components/bwt_perla/util.py new file mode 100644 index 0000000..1473cff --- /dev/null +++ b/custom_components/bwt_perla/util.py @@ -0,0 +1,11 @@ +def truncate_value(value: str, max_length: int = 255) -> str: + """Truncate a string to `max_length` characters, adding ellipsis if needed. + + Returns an empty string for None-like inputs and ensures a string is returned + even if the input cannot be converted to `str`. + """ + if value is None: + return "" + if len(value) <= max_length: + return value + return value[: max_length - 3] + "..." diff --git a/dev/configuration.yaml b/dev/configuration.yaml index 855d3da..36e193f 100644 --- a/dev/configuration.yaml +++ b/dev/configuration.yaml @@ -2,14 +2,6 @@ # Loads default set of integrations. Do not remove. default_config: -# Load frontend themes from the themes folder -frontend: - themes: !include_dir_merge_named themes - -automation: !include automations.yaml -script: !include scripts.yaml -scene: !include scenes.yaml - logger: default: info logs: From d58f63aa59478fa770cff29a62b4bf19fb77d19e Mon Sep 17 00:00:00 2001 From: dkarv Date: Sun, 15 Feb 2026 10:26:34 +0000 Subject: [PATCH 10/10] check this translation format --- custom_components/bwt_perla/sensor.py | 1 + custom_components/bwt_perla/sensors/base.py | 121 ----------------- custom_components/bwt_perla/sensors/error.py | 126 ++++++++++++++++++ custom_components/bwt_perla/strings.json | 90 ++++++------- .../bwt_perla/translations/de.json | 90 ++++++------- .../bwt_perla/translations/en.json | 90 ++++++------- 6 files changed, 259 insertions(+), 259 deletions(-) create mode 100644 custom_components/bwt_perla/sensors/error.py diff --git a/custom_components/bwt_perla/sensor.py b/custom_components/bwt_perla/sensor.py index 05b9d12..9d0813f 100644 --- a/custom_components/bwt_perla/sensor.py +++ b/custom_components/bwt_perla/sensor.py @@ -20,6 +20,7 @@ from .const import DOMAIN from .coordinator import BwtCoordinator from .sensors.base import * +from .sensors.error import * _GLASS = "mdi:cup-water" _COUNTER = "mdi:counter" diff --git a/custom_components/bwt_perla/sensors/base.py b/custom_components/bwt_perla/sensors/base.py index d42e151..dd27a60 100644 --- a/custom_components/bwt_perla/sensors/base.py +++ b/custom_components/bwt_perla/sensors/base.py @@ -19,9 +19,7 @@ from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.helpers import translation -from ..util import truncate_value from ..const import DOMAIN from ..coordinator import BwtCoordinator @@ -30,8 +28,6 @@ _FAUCET = "mdi:faucet" _WATER = "mdi:water" -_WARNING = "mdi:alert-circle" -_ERROR = "mdi:alert-decagram" _WATER_CHECK = "mdi:water-check" _HOLIDAY = "mdi:location-exit" _UNKNOWN = "mdi:help-circle" @@ -55,40 +51,6 @@ def __init__( self._attr_unique_id = entry_id + "_" + key -class TranslatableErrorMixin: - """Mixin for entities that need to translate error codes. - - This mixin provides translation functionality for entities that display - multiple error/warning codes. It loads translations when the entity is - added to Home Assistant and provides a method to translate individual codes. - - Attributes: - _translations: Dictionary of translations loaded from language files. - Initialized as None and populated in async_added_to_hass. - """ - - _translations = None - - async def async_added_to_hass(self) -> None: - """When entity is added to hass, load translations.""" - await super().async_added_to_hass() - # Load translations for the current language - self._translations = await translation.async_get_translations( - self.hass, - self.hass.config.language, - "entity_component", - {DOMAIN}, - ) - - def _translate_code(self, code_name: str) -> str: - """Translate an error/warning code to the user's language.""" - if self._translations is None: - return code_name - - key = f"component.{DOMAIN}.entity_component._.state.error.{code_name.lower()}" - return self._translations.get(key, code_name) - - class TotalOutputSensor(BwtEntity, SensorEntity): """Total water [liter] that passed through the output.""" @@ -132,89 +94,6 @@ def _handle_coordinator_update(self) -> None: self.async_write_ha_state() -class ErrorSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): - """Errors reported by the device.""" - - _attr_icon = _ERROR - - def __init__(self, coordinator, device_info, entry_id) -> None: - """Initialize the sensor with the common coordinator.""" - super().__init__(coordinator, device_info, entry_id, "errors") - self._update_values(self._get_errors()) - - def _get_errors(self): - """Get the current list of fatal errors.""" - return [x for x in self.coordinator.data.errors() if x.is_fatal()] - - async def async_added_to_hass(self) -> None: - """When entity is added to hass, load translations.""" - await super().async_added_to_hass() - # Update values with translations now that they're loaded - self._update_values(self._get_errors()) - self.async_write_ha_state() - - def _update_values(self, errors) -> None: - """Update error values with translations.""" - raw_values = [x.name for x in errors] - # Store raw values as extra attributes for automation - self._attr_extra_state_attributes = {"error_codes": raw_values} - - # Translate error names for display - if errors: - translated = [self._translate_code(x.name) for x in errors] - # Join translated parts and ensure it does not exceed 255 chars - joined = ", ".join(translated) - self._attr_native_value = truncate_value(joined, 255) - else: - self._attr_native_value = "" - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._update_values(self._get_errors()) - self.async_write_ha_state() - - -class WarningSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): - """Warnings reported by the device.""" - - _attr_icon = _WARNING - - def __init__(self, coordinator, device_info, entry_id) -> None: - """Initialize the sensor with the common coordinator.""" - super().__init__(coordinator, device_info, entry_id, "warnings") - self._update_values(self._get_warnings()) - - def _get_warnings(self): - """Get the current list of non-fatal warnings.""" - return [x for x in self.coordinator.data.errors() if not x.is_fatal()] - - async def async_added_to_hass(self) -> None: - """When entity is added to hass, load translations.""" - await super().async_added_to_hass() - # Update values with translations now that they're loaded - self._update_values(self._get_warnings()) - self.async_write_ha_state() - - def _update_values(self, warnings) -> None: - """Update warning values with translations.""" - raw_values = [x.name for x in warnings] - # Store raw values as extra attributes for automation - self._attr_extra_state_attributes = {"warning_codes": raw_values} - - # Translate warning names for display - translated = [self._translate_code(x.name) for x in warnings] - # Join translated parts and ensure it does not exceed 255 chars - joined = ", ".join(translated) - self._attr_native_value = truncate_value(joined, 255) - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._update_values(self._get_warnings()) - self.async_write_ha_state() - - class SimpleSensor(BwtEntity, SensorEntity): """Simplest sensor with least configuration options.""" diff --git a/custom_components/bwt_perla/sensors/error.py b/custom_components/bwt_perla/sensors/error.py new file mode 100644 index 0000000..99a5409 --- /dev/null +++ b/custom_components/bwt_perla/sensors/error.py @@ -0,0 +1,126 @@ +from .base import BwtEntity +from homeassistant.components.sensor import ( + SensorEntity, +) +from ..const import DOMAIN +from ..util import truncate_value + +from homeassistant.core import callback +from homeassistant.helpers import translation + +from bwt_api.error import BwtError + +_WARNING = "mdi:alert-circle" +_ERROR = "mdi:alert-decagram" + +class TranslatableErrorMixin: + """Mixin for entities that need to translate error codes. + + This mixin provides translation functionality for entities that display + multiple error/warning codes. It loads translations when the entity is + added to Home Assistant and provides a method to translate individual codes. + + Attributes: + _translations: Dictionary of translations loaded from language files. + Initialized as None and populated in async_added_to_hass. + """ + + _translations = None + + async def async_added_to_hass(self) -> None: + """When entity is added to hass, load translations.""" + await super().async_added_to_hass() + # Load translations for the current language + self._translations = await translation.async_get_translations( + self.hass, + self.hass.config.language, + "entity_component", + {DOMAIN}, + ) + + def _translate_code(self, code_name: str) -> str: + """Translate an error/warning code to the user's language.""" + if self._translations is None: + return code_name + + key = f"component.{DOMAIN}.entity_component._.state.error_{code_name.lower()}" + return self._translations.get(key, code_name) + +class ErrorSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): + """Errors reported by the device.""" + + _attr_icon = _ERROR + + def __init__(self, coordinator, device_info, entry_id) -> None: + """Initialize the sensor with the common coordinator.""" + super().__init__(coordinator, device_info, entry_id, "errors") + self._update_values(self._get_errors()) + + def _get_errors(self): + """Get the current list of fatal errors.""" + return [x for x in self.coordinator.data.errors() if x.is_fatal()] + + async def async_added_to_hass(self) -> None: + """When entity is added to hass, load translations.""" + await super().async_added_to_hass() + # Update values with translations now that they're loaded + self._update_values(self._get_errors()) + self.async_write_ha_state() + + def _update_values(self, errors) -> None: + """Update error values with translations.""" + raw_values = [x.name for x in errors] + # Store raw values as extra attributes for automation + self._attr_extra_state_attributes = {"error_codes": raw_values} + + # Translate error names for display + translated = [self._translate_code(x.name) for x in errors] + # Join translated parts and ensure it does not exceed 255 chars + joined = ", ".join(translated) + self._attr_native_value = truncate_value(joined, 255) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_values(self._get_errors()) + self.async_write_ha_state() + + +class WarningSensor(TranslatableErrorMixin, BwtEntity, SensorEntity): + """Warnings reported by the device.""" + + _attr_icon = _WARNING + + def __init__(self, coordinator, device_info, entry_id) -> None: + """Initialize the sensor with the common coordinator.""" + super().__init__(coordinator, device_info, entry_id, "warnings") + self._update_values(self._get_warnings()) + + def _get_warnings(self): + """Get the current list of non-fatal warnings.""" + return [x for x in self.coordinator.data.errors() if not x.is_fatal()] + + async def async_added_to_hass(self) -> None: + """When entity is added to hass, load translations.""" + await super().async_added_to_hass() + # Update values with translations now that they're loaded + self._update_values(self._get_warnings()) + self.async_write_ha_state() + + def _update_values(self, warnings) -> None: + """Update warning values with translations.""" + raw_values = [x.name for x in warnings] + # Store raw values as extra attributes for automation + self._attr_extra_state_attributes = {"warning_codes": raw_values} + + # Translate warning names for display + translated = [self._translate_code(x.name) for x in warnings] + # Join translated parts and ensure it does not exceed 255 chars + joined = ", ".join(translated) + self._attr_native_value = truncate_value(joined, 255) + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_values(self._get_warnings()) + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/bwt_perla/strings.json b/custom_components/bwt_perla/strings.json index 67b5c59..9dfe572 100644 --- a/custom_components/bwt_perla/strings.json +++ b/custom_components/bwt_perla/strings.json @@ -105,52 +105,50 @@ "entity_component": { "_": { "state": { - "error": { - "unknown": "Unknown error", - "offline_motor_1": "Motor 1 offline", - "offline_motor_2": "Motor 2 offline", - "offline_motor_blend": "Blend motor offline", - "regenerativ_20": "Regeneration salt level < 20%", - "overcurrent_motor_1": "Overcurrent motor 1", - "overcurrent_motor_2": "Overcurrent motor 2", - "overcurrent_motor_3": "Overcurrent motor 3", - "overcurrent_valve": "Overcurrent valve", - "stop_volume": "Stop volume", - "stop_sensor": "Stop sensor", - "constant_flow": "Constant flow", - "low_pressure": "Low pressure", - "piston_position": "Piston position", - "electronic": "Electronic", - "insufficient_regenerativ": "Insufficient regeneration salt", - "stop_wireless_sensor": "Stop wireless sensor", - "regenerativ_0": "Regeneration salt empty", - "maintenance_customer": "Routine maintenance due", - "inspection_customer": "Customer inspection required", - "maintenance_service": "Technician maintenance due", - "minerals_low": "Minerals low", - "minerals_0": "Minerals empty", - "overcurrent_valve_1": "Overcurrent valve 1", - "overcurrent_valve_2": "Overcurrent valve 2", - "overcurrent_dosing": "Overcurrent dosing", - "overcurrent_valve_ball": "Overcurrent ball valve", - "meter_not_counting": "Water meter not counting", - "regeneration_drain": "Regeneration drain issue", - "init_pcb_0": "PCB initialization 0", - "init_pcb_1": "PCB initialization 1", - "position_motor_1": "Motor 1 position", - "position_motor_2": "Motor 2 position", - "conductivity_high": "Conductivity too high", - "conductivity_limit_1": "Conductivity limit 1 exceeded", - "conductivity_limit_2": "Conductivity limit 2 exceeded", - "conductivity_limit_water": "Water conductivity limit exceeded", - "no_function": "No function", - "temperature_disconnected": "Temperature sensor disconnected", - "temperature_high": "Temperature too high", - "offline_valve_ball": "Ball valve offline", - "external_filter_change": "External filter change required", - "brine_unsaturated": "Brine unsaturated", - "dosing_fault": "Dosing fault" - } + "error_unknown": "Unknown error", + "error_offline_motor_1": "Motor 1 offline", + "error_offline_motor_2": "Motor 2 offline", + "error_offline_motor_blend": "Blend motor offline", + "error_regenerativ_20": "Regeneration salt level < 20%", + "error_overcurrent_motor_1": "Overcurrent motor 1", + "error_overcurrent_motor_2": "Overcurrent motor 2", + "error_overcurrent_motor_3": "Overcurrent motor 3", + "error_overcurrent_valve": "Overcurrent valve", + "error_stop_volume": "Stop volume", + "error_stop_sensor": "Stop sensor", + "error_constant_flow": "Constant flow", + "error_low_pressure": "Low pressure", + "error_piston_position": "Piston position", + "error_electronic": "Electronic", + "error_insufficient_regenerativ": "Insufficient regeneration salt", + "error_stop_wireless_sensor": "Stop wireless sensor", + "error_regenerativ_0": "Regeneration salt empty", + "error_maintenance_customer": "Routine maintenance due", + "error_inspection_customer": "Customer inspection required", + "error_maintenance_service": "Technician maintenance due", + "error_minerals_low": "Minerals low", + "error_minerals_0": "Minerals empty", + "error_overcurrent_valve_1": "Overcurrent valve 1", + "error_overcurrent_valve_2": "Overcurrent valve 2", + "error_overcurrent_dosing": "Overcurrent dosing", + "error_overcurrent_valve_ball": "Overcurrent ball valve", + "error_meter_not_counting": "Water meter not counting", + "error_regeneration_drain": "Regeneration drain issue", + "error_init_pcb_0": "PCB initialization 0", + "error_init_pcb_1": "PCB initialization 1", + "error_position_motor_1": "Motor 1 position", + "error_position_motor_2": "Motor 2 position", + "error_conductivity_high": "Conductivity too high", + "error_conductivity_limit_1": "Conductivity limit 1 exceeded", + "error_conductivity_limit_2": "Conductivity limit 2 exceeded", + "error_conductivity_limit_water": "Water conductivity limit exceeded", + "error_no_function": "No function", + "error_temperature_disconnected": "Temperature sensor disconnected", + "error_temperature_high": "Temperature too high", + "error_offline_valve_ball": "Ball valve offline", + "error_external_filter_change": "External filter change required", + "error_brine_unsaturated": "Brine unsaturated", + "error_dosing_fault": "Dosing fault" } } } diff --git a/custom_components/bwt_perla/translations/de.json b/custom_components/bwt_perla/translations/de.json index aca1f22..d7dba44 100644 --- a/custom_components/bwt_perla/translations/de.json +++ b/custom_components/bwt_perla/translations/de.json @@ -105,52 +105,50 @@ "entity_component": { "_": { "state": { - "error": { - "unknown": "Unbekannter Fehler", - "offline_motor_1": "Motor 1 offline", - "offline_motor_2": "Motor 2 offline", - "offline_motor_blend": "Mischmotor offline", - "regenerativ_20": "Regeneriersalz-Stand < 20%", - "overcurrent_motor_1": "Überstrom Motor 1", - "overcurrent_motor_2": "Überstrom Motor 2", - "overcurrent_motor_3": "Überstrom Motor 3", - "overcurrent_valve": "Überstrom Ventil", - "stop_volume": "Volumen-Stopp", - "stop_sensor": "Sensor-Stopp", - "constant_flow": "Konstanter Durchfluss", - "low_pressure": "Niedriger Druck", - "piston_position": "Kolbenpositionsfehler", - "electronic": "Elektronikfehler", - "insufficient_regenerativ": "Unzureichendes Regeneriersalz", - "stop_wireless_sensor": "Funk-Sensor-Stopp", - "regenerativ_0": "Regeneriersalz leer", - "maintenance_customer": "Kundenwartung erforderlich", - "inspection_customer": "Kundeninspektion erforderlich", - "maintenance_service": "Technikerwartung erforderlich", - "minerals_low": "Mineralien niedrig", - "minerals_0": "Mineralien leer", - "overcurrent_valve_1": "Überstrom Ventil 1", - "overcurrent_valve_2": "Überstrom Ventil 2", - "overcurrent_dosing": "Überstrom Dosierung", - "overcurrent_valve_ball": "Überstrom Kugelventil", - "meter_not_counting": "Wasserzähler zählt nicht", - "regeneration_drain": "Regenerationsabfluss-Problem", - "init_pcb_0": "PCB-Initialisierungsfehler 0", - "init_pcb_1": "PCB-Initialisierungsfehler 1", - "position_motor_1": "Positionsfehler Motor 1", - "position_motor_2": "Positionsfehler Motor 2", - "conductivity_high": "Leitfähigkeit zu hoch", - "conductivity_limit_1": "Leitfähigkeitsgrenze 1 überschritten", - "conductivity_limit_2": "Leitfähigkeitsgrenze 2 überschritten", - "conductivity_limit_water": "Wasser-Leitfähigkeitsgrenze überschritten", - "no_function": "Keine Funktion", - "temperature_disconnected": "Temperatursensor getrennt", - "temperature_high": "Temperatur zu hoch", - "offline_valve_ball": "Kugelventil offline", - "external_filter_change": "Externer Filterwechsel erforderlich", - "brine_unsaturated": "Sole ungesättigt", - "dosing_fault": "Dosierfehler" - } + "error_unknown": "Unbekannter Fehler", + "error_offline_motor_1": "Motor 1 offline", + "error_offline_motor_2": "Motor 2 offline", + "error_offline_motor_blend": "Mischmotor offline", + "error_regenerativ_20": "Regeneriersalz-Stand < 20%", + "error_overcurrent_motor_1": "Überstrom Motor 1", + "error_overcurrent_motor_2": "Überstrom Motor 2", + "error_overcurrent_motor_3": "Überstrom Motor 3", + "error_overcurrent_valve": "Überstrom Ventil", + "error_stop_volume": "Volumen-Stopp", + "error_stop_sensor": "Sensor-Stopp", + "error_constant_flow": "Konstanter Durchfluss", + "error_low_pressure": "Niedriger Druck", + "error_piston_position": "Kolbenpositionsfehler", + "error_electronic": "Elektronikfehler", + "error_insufficient_regenerativ": "Unzureichendes Regeneriersalz", + "error_stop_wireless_sensor": "Funk-Sensor-Stopp", + "error_regenerativ_0": "Regeneriersalz leer", + "error_maintenance_customer": "Kundenwartung erforderlich", + "error_inspection_customer": "Kundeninspektion erforderlich", + "error_maintenance_service": "Technikerwartung erforderlich", + "error_minerals_low": "Mineralien niedrig", + "error_minerals_0": "Mineralien leer", + "error_overcurrent_valve_1": "Überstrom Ventil 1", + "error_overcurrent_valve_2": "Überstrom Ventil 2", + "error_overcurrent_dosing": "Überstrom Dosierung", + "error_overcurrent_valve_ball": "Überstrom Kugelventil", + "error_meter_not_counting": "Wasserzähler zählt nicht", + "error_regeneration_drain": "Regenerationsabfluss-Problem", + "error_init_pcb_0": "PCB-Initialisierungsfehler 0", + "error_init_pcb_1": "PCB-Initialisierungsfehler 1", + "error_position_motor_1": "Positionsfehler Motor 1", + "error_position_motor_2": "Positionsfehler Motor 2", + "error_conductivity_high": "Leitfähigkeit zu hoch", + "error_conductivity_limit_1": "Leitfähigkeitsgrenze 1 überschritten", + "error_conductivity_limit_2": "Leitfähigkeitsgrenze 2 überschritten", + "error_conductivity_limit_water": "Wasser-Leitfähigkeitsgrenze überschritten", + "error_no_function": "Keine Funktion", + "error_temperature_disconnected": "Temperatursensor getrennt", + "error_temperature_high": "Temperatur zu hoch", + "error_offline_valve_ball": "Kugelventil offline", + "error_external_filter_change": "Externer Filterwechsel erforderlich", + "error_brine_unsaturated": "Sole ungesättigt", + "error_dosing_fault": "Dosierfehler" } } } diff --git a/custom_components/bwt_perla/translations/en.json b/custom_components/bwt_perla/translations/en.json index 67b5c59..fa861d5 100644 --- a/custom_components/bwt_perla/translations/en.json +++ b/custom_components/bwt_perla/translations/en.json @@ -105,52 +105,50 @@ "entity_component": { "_": { "state": { - "error": { - "unknown": "Unknown error", - "offline_motor_1": "Motor 1 offline", - "offline_motor_2": "Motor 2 offline", - "offline_motor_blend": "Blend motor offline", - "regenerativ_20": "Regeneration salt level < 20%", - "overcurrent_motor_1": "Overcurrent motor 1", - "overcurrent_motor_2": "Overcurrent motor 2", - "overcurrent_motor_3": "Overcurrent motor 3", - "overcurrent_valve": "Overcurrent valve", - "stop_volume": "Stop volume", - "stop_sensor": "Stop sensor", - "constant_flow": "Constant flow", - "low_pressure": "Low pressure", - "piston_position": "Piston position", - "electronic": "Electronic", - "insufficient_regenerativ": "Insufficient regeneration salt", - "stop_wireless_sensor": "Stop wireless sensor", - "regenerativ_0": "Regeneration salt empty", - "maintenance_customer": "Routine maintenance due", - "inspection_customer": "Customer inspection required", - "maintenance_service": "Technician maintenance due", - "minerals_low": "Minerals low", - "minerals_0": "Minerals empty", - "overcurrent_valve_1": "Overcurrent valve 1", - "overcurrent_valve_2": "Overcurrent valve 2", - "overcurrent_dosing": "Overcurrent dosing", - "overcurrent_valve_ball": "Overcurrent ball valve", - "meter_not_counting": "Water meter not counting", - "regeneration_drain": "Regeneration drain issue", - "init_pcb_0": "PCB initialization 0", - "init_pcb_1": "PCB initialization 1", - "position_motor_1": "Motor 1 position", - "position_motor_2": "Motor 2 position", - "conductivity_high": "Conductivity too high", - "conductivity_limit_1": "Conductivity limit 1 exceeded", - "conductivity_limit_2": "Conductivity limit 2 exceeded", - "conductivity_limit_water": "Water conductivity limit exceeded", - "no_function": "No function", - "temperature_disconnected": "Temperature sensor disconnected", - "temperature_high": "Temperature too high", - "offline_valve_ball": "Ball valve offline", - "external_filter_change": "External filter change required", - "brine_unsaturated": "Brine unsaturated", - "dosing_fault": "Dosing fault" - } + "error_unknown": "Unknown error", + "error_offline_motor_1": "Motor 1 offline", + "error_offline_motor_2": "Motor 2 offline", + "error_offline_motor_blend": "Blend motor offline", + "error_regenerativ_20": "Regeneration salt level < 20%", + "error_overcurrent_motor_1": "Overcurrent motor 1", + "error_overcurrent_motor_2": "Overcurrent motor 2", + "error_overcurrent_motor_3": "Overcurrent motor 3", + "error_overcurrent_valve": "Overcurrent valve", + "error_stop_volume": "Stop volume", + "error_stop_sensor": "Stop sensor", + "error_constant_flow": "Constant flow", + "error_low_pressure": "Low pressure", + "error_piston_position": "Piston position", + "error_electronic": "Electronic", + "error_insufficient_regenerativ": "Insufficient regeneration salt", + "error_stop_wireless_sensor": "Stop wireless sensor", + "error_regenerativ_0": "Regeneration salt empty", + "error_maintenance_customer": "Routine maintenance due", + "error_inspection_customer": "Customer inspection required", + "error_maintenance_service": "Technician maintenance due", + "error_minerals_low": "Minerals low", + "error_minerals_0": "Minerals empty", + "error_overcurrent_valve_1": "Overcurrent valve 1", + "error_overcurrent_valve_2": "Overcurrent valve 2", + "error_overcurrent_dosing": "Overcurrent dosing", + "error_overcurrent_valve_ball": "Overcurrent ball valve", + "error_meter_not_counting": "Water meter not counting", + "error_regeneration_drain": "Regeneration drain issue", + "error_init_pcb_0": "PCB initialization 0", + "error_init_pcb_1": "PCB initialization 1", + "error_position_motor_1": "Motor 1 position", + "error_position_motor_2": "Motor 2 position", + "error_conductivity_high": "Conductivity too high", + "error_conductivity_limit_1": "Conductivity limit 1 exceeded", + "error_conductivity_limit_2": "Conductivity limit 2 exceeded", + "error_conductivity_limit_water": "Water conductivity limit exceeded", + "error_no_function": "No function", + "error_temperature_disconnected": "Temperature sensor disconnected", + "error_temperature_high": "Temperature too high", + "error_offline_valve_ball": "Ball valve offline", + "error_external_filter_change": "External filter change required", + "error_brine_unsaturated": "Brine unsaturated", + "error_dosing_fault": "Dosing fault" } } }