Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Data collected:
* connection related information (amount of data, throughput, daily up/download statistics)
* all log messages
* home automation (temperature, power consumption, heating settings)
* FRITZ!Smart Energy 250 grid meter (import/export direction detection, net power, per-channel energy)
* phone call list (incoming, outgoing, missed, blocked)
* system stats (cpu usage/temp, memory usage)
* WLAN information (num clients, status, channels)
Expand Down Expand Up @@ -233,9 +234,50 @@ InfluxDB >=2.2.0:
* [fritzbox_logs_dashboard.json](https://github.com/bb-Ricardo/fritzinfluxdb/blob/main/grafana/influx2_dashboards/fritzbox_logs_dashboard.json)
* [fritzbox_call_log_dashboard.json](https://github.com/bb-Ricardo/fritzinfluxdb/blob/main/grafana/influx2_dashboards/fritzbox_call_log_dashboard.json)
* [fritzbox_home_automation_dashboard.json](https://github.com/bb-Ricardo/fritzinfluxdb/blob/main/grafana/influx2_dashboards/fritzbox_home_automation_dashboard.json)
* [fritzbox_smart_energy_250_dashboard.json](https://github.com/bb-Ricardo/fritzinfluxdb/blob/main/grafana/influx2_dashboards/fritzbox_smart_energy_250_dashboard.json) — grid import/export monitoring for PV systems

*This was heavily inspired by: [https://grafana.com/grafana/dashboards/713-fritz-box-router-status/](https://grafana.com/grafana/dashboards/713-fritz-box-router-status/)*

## FRITZ!Smart Energy 250

The FRITZ!Smart Energy 250 is a smart meter reader that clips onto the utility meter's optical interface.
It reports two sub-devices via the AHA-HTTP-Interface:

| AIN Suffix | Channel | Measurement |
|-----------|---------|-------------|
| `-1` | Grid import (Bezug, A+) | Energy consumed from the grid |
| `-2` | Grid export (Einspeisung, A-) | Energy fed into the grid (solar) |

Both channels share the same instantaneous power reading. The energy counters (Wh) differ by direction.

### Direction detection

Since the power value alone cannot distinguish import from export, fritzinfluxdb uses a **sliding window comparison** (default: 300 seconds) over the cumulative energy counters of both channels. The channel whose counter grows faster determines the current direction.

At low power levels (< 120 W), a single 30-second interval may not produce a 1 Wh delta — the sliding window accumulates enough energy to detect direction reliably.

### Additional metrics

These metrics are automatically collected when a Smart Energy 250 is detected:

| Metric | Type | Description |
|--------|------|-------------|
| `ha_energy_direction` | string | Current flow direction: `import`, `export`, `balanced`, or `unknown` |
| `ha_energy_direction_numeric` | int | Numeric direction: `1` (import), `-1` (export), `0` (balanced/unknown) |
| `ha_energy_net_power` | float | Signed power in Watt — positive for import, negative for export |

These complement the existing `ha_powermeter_power` and `ha_powermeter_energy` metrics which are collected per channel.

### Grafana Dashboard

A dedicated dashboard is included: `grafana/influx2_dashboards/fritzbox_smart_energy_250_dashboard.json`

Panels:
* Current power, direction indicator, net power stat
* Net power time series (green below zero = export, red above = import)
* Direction timeline (state-timeline panel)
* Hourly import vs. export energy (stacked bar chart)

## Configure more attributes

check here to find an overview of more attributes which probably could be added
Expand Down
107 changes: 107 additions & 0 deletions fritzinfluxdb/classes/fritzbox/energy_direction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2022 - 2023 Ricardo Bartels. All rights reserved.
#
# fritzinfluxdb.py
#
# This work is licensed under the terms of the MIT license.
# For a copy, see file LICENSE.txt included in this
# repository or visit: <https://opensource.org/licenses/MIT>.

"""
Energy direction detection for FRITZ!Smart Energy 250.

The Smart Energy 250 reports two sub-devices via AHA-HTTP-Interface:
- AIN suffix "-1": grid import (Bezug, A+)
- AIN suffix "-2": grid export (Einspeisung, A-)

Both channels report the same instantaneous power value — only the
cumulative energy counters (Wh) differ by direction. Since the counters
have 1 Wh resolution, short polling intervals (< 60s) at low power
levels produce zero deltas. A sliding window (default 300s) accumulates
enough energy to reliably detect direction.
"""

import time
from collections import deque

from fritzinfluxdb.log import get_logger

log = get_logger()


class EnergyDirectionTracker:
"""
Tracks energy counter readings per device and derives the current
energy flow direction from a sliding window comparison.
"""

def __init__(self, window_seconds=300):
self.window_seconds = window_seconds
self._history = {}

def update(self, ain, energy_wh):
now = time.monotonic()
if ain not in self._history:
self._history[ain] = deque()
self._history[ain].append((now, energy_wh))

cutoff = now - self.window_seconds - 30
while self._history[ain] and self._history[ain][0][0] < cutoff:
self._history[ain].popleft()

def get_delta(self, ain):
entries = self._history.get(ain)
if not entries or len(entries) < 2:
return None

now = time.monotonic()
target_time = now - self.window_seconds
oldest = None
for ts, val in entries:
if ts <= target_time:
oldest = (ts, val)
else:
break

if oldest is None:
oldest = entries[0]

newest = entries[-1]
if oldest[0] == newest[0]:
return None

return max(0, newest[1] - oldest[1])


_tracker = EnergyDirectionTracker()


def track_energy_reading(ain, energy_wh):
_tracker.update(ain, energy_wh)


def get_energy_direction(bezug_ain, einsp_ain):
b_delta = _tracker.get_delta(bezug_ain)
e_delta = _tracker.get_delta(einsp_ain)

if b_delta is None or e_delta is None:
return "unknown"

if e_delta > b_delta:
return "export"
elif b_delta > e_delta:
return "import"
else:
return "balanced"


def get_energy_direction_numeric(bezug_ain, einsp_ain):
direction = get_energy_direction(bezug_ain, einsp_ain)
return {"export": -1, "import": 1, "balanced": 0, "unknown": 0}.get(direction, 0)


def get_net_power(power_w, bezug_ain, einsp_ain):
direction = get_energy_direction(bezug_ain, einsp_ain)
if direction == "export":
return -abs(power_w)
return abs(power_w)
83 changes: 83 additions & 0 deletions fritzinfluxdb/classes/fritzbox/service_definitions/homeauto.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from fritzinfluxdb.common import grab, in_test_mode
from fritzinfluxdb.classes.fritzbox.service_handler import FritzBoxLuaURLPath
from fritzinfluxdb.classes.fritzbox.service_definitions import lua_services
from fritzinfluxdb.classes.fritzbox.energy_direction import (
track_energy_reading, get_energy_direction, get_energy_direction_numeric, get_net_power
)

home_automation_device_classes = {
0: "HAN-FUN",
Expand Down Expand Up @@ -134,9 +137,45 @@ def get_ha_powermeter_energy(data):
if in_test_mode():
return float(energy) + float(datetime.now().timestamp() - test_start_ts)

ain = data.get("@identifier", "").strip()
if ain and energy is not None:
track_energy_reading(ain, float(energy))

return energy


def _get_channel_pair(data):
ain = data.get("@identifier", "").strip()
if not ain.endswith("-1"):
return None, None
base = ain[:-2]
return ain, base + "-2"


def get_ha_energy_direction(data):
bezug_ain, einsp_ain = _get_channel_pair(data)
if not bezug_ain:
return None
return get_energy_direction(bezug_ain, einsp_ain)


def get_ha_energy_direction_numeric(data):
bezug_ain, einsp_ain = _get_channel_pair(data)
if not bezug_ain:
return None
return get_energy_direction_numeric(bezug_ain, einsp_ain)


def get_ha_net_power(data):
bezug_ain, einsp_ain = _get_channel_pair(data)
if not bezug_ain:
return None
power = get_ha_powermeter_power(data)
if power is None:
return None
return get_net_power(power, bezug_ain, einsp_ain)


def get_ha_powermeter_voltage(data):

if in_test_mode():
Expand Down Expand Up @@ -569,6 +608,50 @@ def prepare_response_data(response):
"exclude_filter_function": lambda data: "device" not in data.get("devicelist").keys()
},

# Smart Energy 250 — energy direction detection
"ha_energy_direction": {
"data_path": "devicelist.device",
"type": list,
"next": {
"type": str,
"tags_function": lambda data: {"name": data.get("name")},
"value_function": get_ha_energy_direction,
"exclude_filter_function": lambda data: (
"Smart Energy" not in data.get("@productname", "")
or not data.get("@identifier", "").strip().endswith("-1")
)
},
"exclude_filter_function": lambda data: "device" not in data.get("devicelist").keys()
},
"ha_energy_direction_numeric": {
"data_path": "devicelist.device",
"type": list,
"next": {
"type": int,
"tags_function": lambda data: {"name": data.get("name")},
"value_function": get_ha_energy_direction_numeric,
"exclude_filter_function": lambda data: (
"Smart Energy" not in data.get("@productname", "")
or not data.get("@identifier", "").strip().endswith("-1")
)
},
"exclude_filter_function": lambda data: "device" not in data.get("devicelist").keys()
},
"ha_energy_net_power": {
"data_path": "devicelist.device",
"type": list,
"next": {
"type": float,
"tags_function": lambda data: {"name": data.get("name")},
"value_function": get_ha_net_power,
"exclude_filter_function": lambda data: (
"Smart Energy" not in data.get("@productname", "")
or not data.get("@identifier", "").strip().endswith("-1")
)
},
"exclude_filter_function": lambda data: "device" not in data.get("devicelist").keys()
},

# Alarm
"ha_alert": {
"data_path": "devicelist.device",
Expand Down
Loading