Skip to content
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 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 |
Expand Down Expand Up @@ -74,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')`.
1 change: 1 addition & 0 deletions custom_components/bwt_perla/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
45 changes: 5 additions & 40 deletions custom_components/bwt_perla/sensors/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@

from datetime import datetime

import logging

from bwt_api.api import treated_to_blended
from bwt_api.data import BwtStatus

Expand All @@ -18,13 +20,14 @@
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity


from ..const import DOMAIN
from ..coordinator import BwtCoordinator

_LOGGER = logging.getLogger(__name__)

_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"
Expand Down Expand Up @@ -91,44 +94,6 @@ def _handle_coordinator_update(self) -> None:
self.async_write_ha_state()


class ErrorSensor(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")
values = [x.name for x in self.coordinator.data.errors() if x.is_fatal()]
self._attr_native_value = ",".join(values)

@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)
self.async_write_ha_state()


class WarningSensor(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")
values = [x.name for x in self.coordinator.data.errors() if not x.is_fatal()]
self._attr_native_value = ",".join(values)

@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)
self.async_write_ha_state()


class SimpleSensor(BwtEntity, SensorEntity):
"""Simplest sensor with least configuration options."""

Expand Down
126 changes: 126 additions & 0 deletions custom_components/bwt_perla/sensors/error.py
Original file line number Diff line number Diff line change
@@ -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()
Loading