Extension points: New device profiles, calculated and combined data points¶
This guide explains how to extend AioHomematic with:
- Custom device profiles (model/custom)
- Calculated (derived) data points (model/calculated)
- Combined (multi-parameter writable) data points (model/combined)
It targets contributors who want to add support for new device variants, expose derived metrics, or create writable data points that combine multiple underlying parameters.
Prerequisites and conventions¶
- Be familiar with the architecture overview in docs/architecture.md (Model, Device/Channel, DataPoint lifecycle).
- Prefer small, well-scoped additions. Follow existing naming conventions and module layout.
- Keep public API stable. New types should be added behind existing factory/registration functions.
Custom device profiles (model/custom)¶
Custom device profiles are used when a specific device (model) requires a bespoke grouping of its generic data points or additional behavior beyond the generic defaults.
Key modules and types:¶
- aiohomematic.model.custom.registry
DeviceProfileRegistry: Central registry for device-to-profile mappingsDeviceConfig: Type-safe configuration for device registrationExtendedDeviceConfig: Extended configuration with additional fields- aiohomematic.model.custom.definition
make_custom_data_point(channel, data_point_class, device_profile, custom_config): Factory functionis_multi_channel_device(model, category): Check for multi-channel devicesget_custom_configs(model, category): Get configurations for a model- aiohomematic.model.custom.data_point.CustomDataPoint: Base implementation that:
- Groups multiple generic data points, sets visibility, service flags, etc.
- Subscribes to underlying GenericDataPoint updates
- Provides state_property values derived from the grouped set
- aiohomematic.const: Contains
Field,DeviceProfile, andCDPDkeys used in profile definitions
Concepts:¶
- A CustomDataPoint instance sits on a Channel and aggregates underlying GenericDataPoints according to a device profile definition.
- Device profiles are declared in
model/custom/profile.pyas type-safe dataclasses. - Device-to-profile mappings are registered via
DeviceProfileRegistryin each entity module.
When to create a custom device profile:¶
- Generic model would misrepresent or hide essential device semantics.
- You need to group multiple parameters across one or more channels into a single coherent data point.
Steps to add a custom device profile:¶
-
Choose or create a CustomDataPoint subclass (optional)
-
Most cases can use an existing CustomDataPoint subclass (e.g.,
CustomDpIpThermostat,CustomDpSwitch). -
If you need special behavior, subclass CustomDataPoint and override:
- Use
DataPointFielddescriptors for declarative field definitions - Override
_post_init()for additional initialization after field resolution _readable_data_points/_relevant_data_points(to tune exposure)state_propertygetters if you compute an aggregate state
- Use
-
Register the device with DeviceProfileRegistry
-
In the appropriate entity module (e.g.,
climate.py,switch.py), add a registration call:
from aiohomematic.model.custom.registry import DeviceProfileRegistry, ExtendedDeviceConfig
DeviceProfileRegistry.register(
category=DataPointCategory.CLIMATE,
models=("HmIP-NEW-DEVICE", "HmIP-NEW-DEVICE-2"), # Device model(s)
data_point_class=CustomDpIpThermostat, # CustomDataPoint subclass
profile_type=DeviceProfile.IP_THERMOSTAT, # Profile type from const.py
channels=(1,), # Primary channel(s)
schedule_channel_no=1, # Optional: schedule channel
extended=ExtendedDeviceConfig( # Optional: extended config
additional_data_points={
0: (Parameter.SOME_PARAM,),
},
),
)
- For multiple configurations per device, use
register_multiple:
DeviceProfileRegistry.register_multiple(
category=DataPointCategory.LOCK,
models="HmIP-DLD",
configs=(
DeviceConfig(
data_point_class=CustomDpIpLock,
profile_type=DeviceProfile.IP_LOCK,
),
DeviceConfig(
data_point_class=CustomDpButtonLock,
profile_type=DeviceProfile.IP_BUTTON_LOCK,
channels=(0,),
),
),
)
- Validate
- Run the project tests to ensure your device is correctly registered.
- Add specific tests for your device if possible.
Minimal example (adding a new switch device):¶
# In aiohomematic/model/custom/switch.py
from aiohomematic.const import DataPointCategory, DeviceProfile
from aiohomematic.model.custom.registry import DeviceProfileRegistry
# Register the new device
DeviceProfileRegistry.register(
category=DataPointCategory.SWITCH,
models="HmIP-MY-NEW-SWITCH",
data_point_class=CustomDpSwitch,
profile_type=DeviceProfile.IP_SWITCH,
channels=(3,), # Channel number where STATE parameter lives
)
Tips:¶
- Look at existing registrations in
climate.py,switch.py,cover.py, etc. for patterns. - Use
ExtendedDeviceConfigwhen you need additional data points beyond the profile defaults. - For multi-channel devices, specify all relevant channels in the
channelstuple. - To blacklist a device model, use
DeviceProfileRegistry.blacklist("MODEL-NAME").
Calculated (derived) data points (model/calculated)¶
Calculated data points compute values from one or more underlying GenericDataPoints and behave like read-only data points on a Channel.
Key modules and types:¶
- aiohomematic.model.calculated.field.CalculatedDataPointField: Descriptor for declarative field definitions
parameter: The parameter name to resolveparamset_key: The paramset key (VALUES, MASTER, etc.)dpt: Expected data point type (e.g., DpSensor, DpFloat)fallback_parameters: Optional list of fallback parameter namesuse_device_fallback: If True, tries device address (channel 0) if not found- aiohomematic.model.calculated.data_point.CalculatedDataPoint: Base class to inherit from
_resolve_data_point(...)/_add_device_data_point(...)for manual data point resolutionpublish_data_point_updated_eventis triggered when any source updates- Decorators: @state_property, @config_property
- **aiohomematic.model.calculated.**init****
create_calculated_data_points(channel): factory that evaluates relevance and attaches instances to channels_CALCULATED_DATA_POINTS: tuple of registered calculated DP classes- Existing implementations for reference:
- climate.py: ApparentTemperature, DewPoint, FrostPoint, VaporConcentration
- operating_voltage_level.py: OperatingVoltageLevel
Lifecycle:¶
- On channel initialization,
create_calculated_data_points(channel)iterates all registered classes, callsClass.is_relevant_for_model(channel)and adds instances for those that apply. - Each CalculatedDataPoint uses
CalculatedDataPointFielddescriptors for lazy data point resolution with automatic subscription handling. - When any source data point updates, the calculated data point's value is recomputed.
Steps to add a new calculated data point:¶
-
Implement a subclass of CalculatedDataPoint
-
Set
_calculated_parameterto a value fromaiohomematic.const.CalculatedParameter - Use
CalculatedDataPointFielddescriptors to declare source data points: - For fallback parameters (try alternatives if primary not found):
- For device-level fallback (try device address if not on channel):
- Provide properties using decorators:
@state_property def value(self) -> T:return computed value@config_property def unit(self) -> str | None:return unit string
- Implement
staticmethod is_relevant_for_model(*, channel: ChannelProtocol) -> boolto guard which channels get this DP -
Override
_post_init()for additional initialization after descriptor resolution -
Register your class
-
Add the class to
_CALCULATED_DATA_POINTSinaiohomematic.model.calculated.__init__: -
Ensure correctness
- The base class manages subscriptions automatically via descriptors
- Use helper functions in
aiohomematic.model.calculated.supportfor common calculations
Minimal template:¶
# aiohomematic/model/calculated/my_metric.py
from __future__ import annotations
from aiohomematic.const import CalculatedParameter, Parameter, ParameterType, ParamsetKey
from aiohomematic.interfaces.model import ChannelProtocol
from aiohomematic.model.calculated.data_point import CalculatedDataPoint
from aiohomematic.model.calculated.field import CalculatedDataPointField
from aiohomematic.model.generic import DpSensor
from aiohomematic.property_decorators import state_property
class MyMetric(CalculatedDataPoint[float | None]):
"""Calculate a custom metric from temperature and humidity."""
__slots__ = ()
_calculated_parameter = CalculatedParameter.MY_METRIC
# Declarative field definitions using descriptors
_dp_temp = CalculatedDataPointField(
parameter=Parameter.TEMPERATURE,
paramset_key=ParamsetKey.VALUES,
dpt=DpSensor,
)
_dp_hum = CalculatedDataPointField(
parameter=Parameter.HUMIDITY,
paramset_key=ParamsetKey.VALUES,
dpt=DpSensor,
)
def __init__(self, *, channel: ChannelProtocol) -> None:
"""Initialize the data point."""
super().__init__(channel=channel)
self._type = ParameterType.FLOAT
self._unit = "unit"
@staticmethod
def is_relevant_for_model(*, channel: ChannelProtocol) -> bool:
"""Return if this calculated data point is relevant for the model."""
return (
channel.get_generic_data_point(
parameter=Parameter.TEMPERATURE, paramset_key=ParamsetKey.VALUES
)
is not None
and channel.get_generic_data_point(
parameter=Parameter.HUMIDITY, paramset_key=ParamsetKey.VALUES
)
is not None
)
@state_property
def value(self) -> float | None:
"""Return the calculated value."""
if self._dp_temp.value is None or self._dp_hum.value is None:
return None
# Implement your calculation here
return (self._dp_temp.value + self._dp_hum.value) / 2
Notes:¶
- Use
use_device_fallback=Trueor_add_device_data_point(...)if you need to read from other channels of the same device. - The base class exposes helper attributes like
self._unit,_min/_max, etc., which you can set in__init__(). - Override
_post_init()for additional initialization that depends on resolved data points. - Keep calculations side-effect free. The base class handles event subscriptions automatically.
Combined (multi-parameter writable) data points (model/combined)¶
Combined data points write to multiple underlying data points and present a single writable value. They are the writable counterpart to calculated data points.
Key modules and types:¶
- aiohomematic.model.combined.data_point.CombinedDataPoint: Base class for combined data points
- Parameterized by
ParameterT(the value type, e.g.,float | None) - Manages
_data_pointsdict mappingFieldto underlying generic data points - Subscribes to underlying data point updates and publishes combined events
- Uses
ParamsetKey.COMBINEDto distinguish from generic and calculated data points - aiohomematic.model.combined.timer.CombinedDpTimerAction: Concrete implementation for timer value+unit pairs
- Accepts values in seconds, converts to optimal unit (S/M/H) via
recalc_unit_timer - Computes
maxasraw_max * 3600when a unit data point exists (hours to seconds) - Persists the seconds value in
_current_value(ACTION params have no CCU events) - aiohomematic.model.combined.field.CombinedTimerField: Descriptor for use in CustomDataPoint subclasses
- Declares a timer value+unit pair with optional visibility
- Creates
CombinedDpTimerActioninstances duringCustomDataPoint._create_combined_data_points() - When
visible=True, the combined data point is registered as a visible HA entity - aiohomematic.interfaces.model.CombinedDataPointProtocol: Protocol interface for combined data points
Lifecycle:¶
- During
CustomDataPoint.__init__(), after_init_data_points()resolves generic data points,_create_combined_data_points()iterates class descriptors marked with_is_combined_timer_fieldand createsCombinedDpTimerActioninstances. - Combined data points are stored in
CustomDataPoint._combined_data_points(keyed byField). - When
visible=True, the combined data point is added to the channel viaChannel.add_data_point()and stored inChannel._combined_data_points. - The combined data point subscribes to its underlying generic data points and publishes update events when they change.
Usage in CustomDataPoint subclasses:¶
from aiohomematic.model.combined.field import CombinedTimerField
class CustomDpSwitch(StateChangeTimerMixin, GroupStateMixin, CustomDataPoint):
"""Class for Homematic switch data point."""
__slots__ = ()
# Timer with value only (no unit DP) — internal, not visible in HA
_dp_on_time = CombinedTimerField(value_field=Field.ON_TIME_VALUE)
@bind_collector
async def turn_on(self, *, on_time: float | None = None, collector: CallParameterCollector | None = None) -> None:
"""Turn the switch on."""
if (timer := self.get_and_start_timer()) is not None:
await self._dp_on_time.send_value(value=timer, collector=collector)
await self._dp_state.turn_on(collector=collector)
class CustomDpIpSiren(CustomDataPoint):
"""Class for Homematic IP siren data point."""
__slots__ = ()
# Timer with value + unit — visible as HA number entity
_dp_duration: Final = CombinedTimerField(
value_field=Field.DURATION, unit_field=Field.DURATION_UNIT, visible=True
)
Key differences from CalculatedDataPoint:¶
| Aspect | CalculatedDataPoint | CombinedDataPoint |
|---|---|---|
| Direction | Read-only (computed) | Writable (sends to CCU) |
| Registration | Via _CALCULATED_DATA_POINTS tuple | Via CombinedTimerField descriptors on CustomDataPoint |
| ParamsetKey | CALCULATED | COMBINED |
| Visibility | Always visible | Configurable via visible parameter |
| Protocol | CalculatedDataPointProtocol | CombinedDataPointProtocol |
Testing and validation¶
- Run the test suite (see README.md for instructions) and add targeted tests for your new profile or calculated DP.
- For calculated DPs, add unit tests around your formula and a small channel/device stub if possible.
- For custom profiles, test that required GenericDataPoints are attached and visible fields behave as expected.
Documentation and discoverability¶
- After adding a new calculated data point, update aiohomematic.model.calculated.init _CALCULATED_DATA_POINTS.
- If you add a reusable helper or pattern, include a short docstring and cross-link from this page.
Where to look for examples¶
- model/calculated/climate.py and operating_voltage_level.py
- model/custom/definition.py and model/custom/data_point.py
If anything in this guide is unclear, open an issue or PR with questions and we’ll help refine these docs.