ADR 0019: Derived Binary Sensors from Enum Data Points¶
Status¶
Proposed (2026-01-28)
Context¶
Problem¶
Some Homematic devices expose enum parameters that are correctly displayed as enum sensors in Home Assistant. However, for certain use cases it would be beneficial to additionally expose these as binary sensors with configurable mapping rules.
Example 1: HmIP-SRH (Window Handle Sensor)
| Parameter | Enum Values | Current HA Entity |
|---|---|---|
| STATE | CLOSED, TILTED, OPEN | Enum sensor |
Desired Additional Entity: Binary sensor where:
OFFwhenSTATE == CLOSEDONwhenSTATE == TILTEDorSTATE == OPEN
Example 2: HmIP-SWSD (Smoke Detector)
| Parameter | Enum Values | Current HA Entity |
|---|---|---|
| SMOKE_DETECTOR_ALARM_STATUS | IDLE_OFF, PRIMARY_ALARM, etc. | Enum sensor |
Desired Additional Entity: Binary sensor where:
OFFwhenSMOKE_DETECTOR_ALARM_STATUS == IDLE_OFFONfor all other alarm states
Current State¶
The existing CustomDpIpSirenSmoke class already implements a similar pattern using an is_on property:
# In aiohomematic/model/custom/siren.py
@state_property
def is_on(self) -> bool | None:
"""Return if siren is on."""
return bool(self._dp_smoke_detector_alarm_status.value != _SMOKE_DETECTOR_ALARM_STATUS_IDLE_OFF)
However, this approach:
- Requires a full custom data point class for each device
- Does not expose a separate binary sensor entity in Home Assistant
- Is not declarative or reusable
Requirements¶
- Code-Based Definition: Mapping rules must be defined in code (not user-configurable)
- Declarative: Similar to the existing
DeviceProfileRegistrypattern - Flexible Mapping: Support for mapping multiple enum values to ON or OFF states
- Device Identification: Rule and entity is only created for specific device models
- Coexistence: Both the original enum sensor and the derived binary sensor must exist
- Event Propagation: Updates to the source enum must trigger updates on the derived binary sensor
Decision¶
Implement a Derived Binary Sensor system using the existing Calculated Data Point pattern with a specialized registry for enum-to-binary mappings.
Key Principles¶
- Reuse Existing Infrastructure: Build on
CalculatedDataPointbase class - Declarative Registration: Define mappings via a registry similar to
DeviceProfileRegistry - Single Source of Truth: Derived binary sensor subscribes to source enum data point
- No Custom Classes Per Device: Generic
DerivedBinarySensorclass with data-driven mappings
Architecture¶
Data Flow¶
┌─────────────────────────────────────┐
│ CCU Backend │
└──────────────┬──────────────────────┘
│ EVENT: STATE = "TILTED"
↓
┌─────────────────────────────────────┐
│ Generic Data Point (DpSelect) │
│ Parameter: STATE │
│ Value: "TILTED" │
└──────────────┬──────────────────────┘
│ subscribe_to_internal_data_point_updated
↓
┌─────────────────────────────────────┐
│ DerivedBinarySensor │
│ (CalculatedDataPoint subclass) │
│ │
│ Mapping: ON = {"TILTED", "OPEN"} │
│ Value: True (ON) │
└─────────────────────────────────────┘
│ publish_data_point_updated_event
↓
┌─────────────────────────────────────┐
│ Home Assistant Binary Sensor │
│ State: ON │
└─────────────────────────────────────┘
Registry Structure¶
A new registry for derived binary sensor definitions:
# aiohomematic/model/calculated/derived_binary_sensor.py
@dataclass(frozen=True, kw_only=True, slots=True)
class DerivedBinarySensorMapping:
"""Definition of a derived binary sensor mapping rule."""
model: str | tuple[str, ...] # Device model(s) this applies to
source_parameter: Parameter # Source enum parameter
source_channel_no: int # Channel where source parameter lives
on_values: frozenset[str] # Enum values that map to ON (binary True)
off_values: frozenset[str] | None = None # Optional: explicit OFF values
# If None: all values not in on_values
calculated_parameter: CalculatedParameter # Unique identifier for derived DP
# Registration API
class DerivedBinarySensorRegistry:
"""Registry for derived binary sensor mappings."""
@classmethod
def register(
cls,
*,
model: str | tuple[str, ...],
source_parameter: Parameter,
source_channel_no: int,
on_values: frozenset[str],
calculated_parameter: CalculatedParameter,
off_values: frozenset[str] | None = None,
) -> None:
"""Register a derived binary sensor mapping."""
...
@classmethod
def get_mappings_for_model(cls, *, model: str) -> tuple[DerivedBinarySensorMapping, ...]:
"""Return all derived binary sensor mappings for a device model."""
...
Example Registrations¶
# In aiohomematic/model/calculated/derived_binary_sensor.py
# HmIP-SRH: Window Handle Sensor → Window Open Binary Sensor
DerivedBinarySensorRegistry.register(
model="HmIP-SRH",
source_parameter=Parameter.STATE,
source_channel_no=1,
on_values=frozenset({"TILTED", "OPEN"}),
calculated_parameter=CalculatedParameter.WINDOW_OPEN,
)
# HmIP-SWSD: Smoke Detector → Smoke Alarm Binary Sensor
DerivedBinarySensorRegistry.register(
model="HmIP-SWSD",
source_parameter=Parameter.SMOKE_DETECTOR_ALARM_STATUS,
source_channel_no=1,
on_values=frozenset({"PRIMARY_ALARM", "INTRUSION_ALARM", "SECONDARY_ALARM"}),
calculated_parameter=CalculatedParameter.SMOKE_ALARM,
)
Generic Implementation Class¶
class DerivedBinarySensor(CalculatedDataPoint[bool | None]):
"""
Calculated binary sensor derived from an enum data point.
This class implements a generic derived binary sensor that maps
enum values to boolean states based on declarative mapping rules.
"""
__slots__ = ("_mapping", "_dp_source")
_category = DataPointCategory.BINARY_SENSOR
def __init__(
self,
*,
channel: ChannelProtocol,
mapping: DerivedBinarySensorMapping,
) -> None:
"""Initialize the derived binary sensor."""
self._mapping: Final = mapping
self._calculated_parameter = mapping.calculated_parameter
super().__init__(channel=channel)
self._type = ParameterType.BOOL
# Resolve source data point
self._dp_source = self._add_data_point(
parameter=mapping.source_parameter,
paramset_key=ParamsetKey.VALUES,
dpt=DpSelect,
)
@staticmethod
def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
"""Return if any derived binary sensor is relevant for this channel."""
# Delegated to factory - this method checks individual mappings
return False # Factory handles relevance
@staticmethod
def is_relevant_for_mapping(
*,
channel: ChannelProtocol,
mapping: DerivedBinarySensorMapping,
) -> bool:
"""Return if a specific mapping is relevant for this channel."""
# Check model match
if isinstance(mapping.model, tuple):
if not element_matches_key(search_elements=mapping.model, compare_with=channel.device.model):
return False
elif not element_matches_key(search_elements=(mapping.model,), compare_with=channel.device.model):
return False
# Check channel match
if channel.no != mapping.source_channel_no:
return False
# Check source parameter exists
return channel.get_generic_data_point(
parameter=mapping.source_parameter,
paramset_key=ParamsetKey.VALUES,
) is not None
@state_property
def value(self) -> bool | None:
"""Return the derived binary value."""
if (source_value := self._dp_source.value) is None:
return None
return source_value in self._mapping.on_values
Factory Integration¶
The factory for creating derived binary sensors integrates with the existing device initialization flow:
# In aiohomematic/model/calculated/__init__.py
def create_derived_binary_sensors(
*,
channel: ChannelProtocol,
) -> tuple[DerivedBinarySensor, ...]:
"""Create all relevant derived binary sensors for a channel."""
derived_sensors: list[DerivedBinarySensor] = []
for mapping in DerivedBinarySensorRegistry.get_mappings_for_model(model=channel.device.model):
if DerivedBinarySensor.is_relevant_for_mapping(channel=channel, mapping=mapping):
derived_sensors.append(
DerivedBinarySensor(channel=channel, mapping=mapping)
)
return tuple(derived_sensors)
New Calculated Parameters¶
Add new calculated parameters for derived binary sensors:
# In aiohomematic/const.py
class CalculatedParameter(StrEnum):
"""Enum with calculated Homematic parameters."""
# Existing
APPARENT_TEMPERATURE = "APPARENT_TEMPERATURE"
DEW_POINT = "DEW_POINT"
DEW_POINT_SPREAD = "DEW_POINT_SPREAD"
ENTHALPY = "ENTHALPY"
FROST_POINT = "FROST_POINT"
OPERATING_VOLTAGE_LEVEL = "OPERATING_VOLTAGE_LEVEL"
VAPOR_CONCENTRATION = "VAPOR_CONCENTRATION"
# New: Derived binary sensors
SMOKE_ALARM = "SMOKE_ALARM"
WINDOW_OPEN = "WINDOW_OPEN"
# Future: Add more as needed
Consequences¶
Positive¶
- Declarative: Mapping rules are data, not code - easy to add new device support
- Reusable: Single implementation class serves all derived binary sensors
- Consistent: Uses existing calculated data point infrastructure
- Maintainable: New device mappings require only registry entries
- Type-Safe: Full mypy support through existing patterns
- Event-Driven: Automatic update propagation via existing subscription mechanism
- No HA Changes: Works with existing Home Assistant binary_sensor platform
Negative¶
- Additional Entities: Each derived sensor is a separate entity in HA
- Enum Values Coupling: Mapping rules must match exact CCU enum value strings
- Calculated Parameter Growth: Each unique derived sensor type needs a new enum value
Risks and Mitigations¶
| Risk | Mitigation |
|---|---|
| Enum values vary by firmware | Document CCU firmware version requirements per mapping |
| Duplicate entities | Clear naming via calculated_parameter for disambiguation |
| Stale mappings | Unit tests verify all registered source parameters exist |
| Performance overhead | Minimal: one additional subscription per derived sensor |
Alternatives Considered¶
Alternative 1: Extend Custom Data Points¶
Add derived binary sensors as additional properties on existing custom data point classes.
Rejected:
- Requires custom class per device (more code)
- Does not create separate HA entity (property, not entity)
- Not declarative
Alternative 2: Home Assistant Template Sensors¶
Let users create template binary sensors in HA configuration.
Rejected:
- User configuration burden
- Not discoverable
- Inconsistent across installations
Alternative 3: Visibility Rules with Device Class Override¶
Use visibility rules to create binary sensor variants of enum sensors.
Rejected:
- Visibility rules control exposure, not transformation
- Would require fundamental changes to data point typing
- Conflates filtering with transformation
Alternative 4: Generic Calculated Data Point with External Config¶
Make mappings configurable via YAML or JSON file.
Rejected:
- Adds configuration complexity
- External file management
- Harder to validate at startup
- Against requirement "only in code"
Implementation¶
Status: Proposed (NOT yet implemented)
When Implemented:
New Files:
aiohomematic/model/calculated/derived_binary_sensor.py- Registry, mapping dataclass, and implementation class
Modified Files:
aiohomematic/const.py- Add newCalculatedParameterenum valuesaiohomematic/model/calculated/__init__.py- Re-export and factory integrationaiohomematic/model/device.py- Call factory during device initialization
Key Components:
DerivedBinarySensorMapping- Declarative mapping definitionDerivedBinarySensorRegistry- Central registry for mappingsDerivedBinarySensor- Generic calculated data point implementationcreate_derived_binary_sensors()- Factory function
Testing:
- Unit tests for registry lookup (exact match, prefix match)
- Unit tests for value mapping (ON values, OFF values, None handling)
- Unit tests for
is_relevant_for_mapping()logic - Integration tests with HmIP-SRH and HmIP-SWSD devices
- Tests verifying event propagation from source to derived sensor
Adding New Derived Binary Sensors¶
To add support for a new device/parameter combination:
- Identify the source parameter and its enum values
- Add CalculatedParameter enum value (if new sensor type):
- Register the mapping:
# In aiohomematic/model/calculated/derived_binary_sensor.py
DerivedBinarySensorRegistry.register(
model="HmIP-XYZ",
source_parameter=Parameter.MY_ENUM_PARAM,
source_channel_no=1,
on_values=frozenset({"VALUE_A", "VALUE_B"}),
calculated_parameter=CalculatedParameter.MY_NEW_SENSOR,
)
- Add tests:
# In tests/test_model_calculated_derived_binary_sensor.py
async def test_my_new_sensor_mapping(...) -> None:
...
- Update changelog
References¶
- CalculatedDataPoint Implementation - Base class for calculated data points
- OperatingVoltageLevel - Example calculated data point
- DeviceProfileRegistry - Similar registry pattern
- CustomDpIpSirenSmoke - Current workaround with
is_onproperty
Created: 2026-01-28 Author: Architecture Review