|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -from datetime import timedelta |
| 3 | +from datetime import datetime, timedelta |
4 | 4 | from decimal import Decimal |
5 | 5 | import inspect |
6 | 6 | import logging |
|
10 | 10 | from homeassistant.const import ( |
11 | 11 | ATTR_UNIT_OF_MEASUREMENT, |
12 | 12 | CONF_NAME, |
| 13 | + STATE_UNAVAILABLE, |
| 14 | + STATE_UNKNOWN, |
13 | 15 | UnitOfEnergy, |
14 | 16 | UnitOfPower, |
15 | 17 | UnitOfTime, |
16 | 18 | ) |
17 | | -from homeassistant.core import HomeAssistant, callback |
| 19 | +from homeassistant.core import HomeAssistant, State, callback |
18 | 20 | from homeassistant.helpers.device_registry import DeviceInfo |
19 | 21 | from homeassistant.helpers.entity import EntityCategory |
20 | 22 | import homeassistant.helpers.entity_registry as er |
|
25 | 27 | ATTR_SOURCE_DOMAIN, |
26 | 28 | ATTR_SOURCE_ENTITY, |
27 | 29 | CONF_DISABLE_EXTENDED_ATTRIBUTES, |
| 30 | + CONF_ENERGY_FILTER_OUTLIER_ENABLED, |
| 31 | + CONF_ENERGY_FILTER_OUTLIER_MAX, |
28 | 32 | CONF_ENERGY_INTEGRATION_METHOD, |
29 | 33 | CONF_ENERGY_SENSOR_CATEGORY, |
30 | 34 | CONF_ENERGY_SENSOR_ID, |
|
40 | 44 | ) |
41 | 45 | from custom_components.powercalc.device_binding import get_device_info |
42 | 46 | from custom_components.powercalc.errors import SensorConfigurationError |
| 47 | +from custom_components.powercalc.filter.outlier import OutlierFilter |
43 | 48 |
|
44 | 49 | from .abstract import ( |
45 | 50 | BaseEntity, |
@@ -280,6 +285,34 @@ def __init__( |
280 | 285 | self._attr_suggested_display_precision = round_digits |
281 | 286 | if entity_category: |
282 | 287 | self._attr_entity_category = EntityCategory(entity_category) |
| 288 | + self._filter_outliers = bool(sensor_config.get(CONF_ENERGY_FILTER_OUTLIER_ENABLED, False)) |
| 289 | + self._outlier_filter = OutlierFilter( |
| 290 | + window_size=30, |
| 291 | + min_samples=5, |
| 292 | + max_z_score=3.5, |
| 293 | + max_expected_step=sensor_config.get(CONF_ENERGY_FILTER_OUTLIER_MAX, 1000), |
| 294 | + ) |
| 295 | + |
| 296 | + def _integrate_on_state_change( |
| 297 | + self, |
| 298 | + old_timestamp: datetime | None, |
| 299 | + new_timestamp: datetime | None, |
| 300 | + old_state: State | None, |
| 301 | + new_state: State | None, |
| 302 | + ) -> None: |
| 303 | + """Override to add outlier filtering.""" |
| 304 | + |
| 305 | + if self._filter_outliers and new_state is not None: |
| 306 | + valid_state = new_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) |
| 307 | + if valid_state and not self._outlier_filter.accept(float(new_state.state)): |
| 308 | + _LOGGER.debug( |
| 309 | + "%s: Rejecting power value %s as outlier for energy integration", |
| 310 | + self.entity_id, |
| 311 | + new_state.state, |
| 312 | + ) |
| 313 | + return |
| 314 | + |
| 315 | + super()._integrate_on_state_change(old_timestamp, new_timestamp, old_state, new_state) |
283 | 316 |
|
284 | 317 | @property |
285 | 318 | def extra_state_attributes(self) -> dict[str, str] | None: |
|
0 commit comments