Files

2.2 MiB

In [37]:
import pandas as pd
from datetime import datetime
from zoneinfo import ZoneInfo

from utils.database import get_state

local_tz = ZoneInfo("Europe/Copenhagen")

df = get_state(
    entity_id="sensor.0x54ef44100140aacb_humidity",
    # limit=100,
    start_time=datetime(2026, 1, 22, 0, 0, tzinfo=local_tz),
    end_time=datetime(2026, 1, 25, 20, 0, tzinfo=local_tz),
)

# Convert time to Copenhagen timezone
df["time"] = df["time"].dt.tz_convert(local_tz)

# Convert state to numeric, forcing errors to NaN
df["state"] = pd.to_numeric(df["state"], errors="coerce")

# Reverse dataframe for chronological order
df = df.iloc[::-1].reset_index(drop=True)

df.info()
<class 'pandas.DataFrame'>
RangeIndex: 3647 entries, 0 to 3646
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype                            
---  ------  --------------  -----                            
 0   time    3647 non-null   datetime64[ns, Europe/Copenhagen]
 1   state   3643 non-null   float64                          
dtypes: datetime64[ns, Europe/Copenhagen](1), float64(1)
memory usage: 57.1 KB
In [38]:
import plotly.express as px

# Create interactive line plot
fig = px.line(
    df,
    x="time",
    y="state",
    title="Humidity Over Time",
    labels={"time": "Time", "state": "Humidity (%)"},
)

# Customize hover template
fig.update_traces(hovertemplate="<b>Time:</b> %{x}<br><b>Humidity:</b> %{y:.2f} %<extra></extra>")
fig.update_layout(height=600, hovermode="x unified")

fig.show()
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]
In [39]:
# Calculate running average with time-based window
window_size = "1min"  # Can use: '1min', '5min', '15min', '1h', etc.
df["state_smoothed"] = df.rolling(window=window_size, on="time")["state"].mean()

# Create plot with both raw and smoothed data
fig = px.line(
    df,
    x="time",
    y=["state", "state_smoothed"],
    title=f"Humidity - Raw vs Smoothed (window={window_size})",
    labels={"time": "Time", "value": "Humidity (%)"},
)

# Update trace names
fig.data[0].name = "Raw"
fig.data[1].name = "Smoothed"

# Customize hover template
fig.update_traces(hovertemplate="<b>Time:</b> %{x}<br><b>Humidity:</b> %{y:.2f} %<extra></extra>")

fig.update_layout(height=600, hovermode="x unified", legend_title_text="Data Type")

fig.show()
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]
In [40]:
# Calculate baseline humidity as mean of last hours
window_size = "24h"
df["baseline_humidity"] = df.rolling(window=window_size, on="time")["state"].mean()

# Create plot with both raw and baseline
fig = px.line(
    df,
    x="time",
    y=["state", "baseline_humidity"],
    title=f"Humidity - Smoothed vs Baseline (window={window_size})",
    labels={"time": "Time", "value": "Humidity (%)"},
)

# Update trace names
fig.data[0].name = "Smoothed"
fig.data[1].name = "Baseline"

# Customize hover template
fig.update_traces(hovertemplate="<b>Time:</b> %{x}<br><b>Humidity:</b> %{y:.2f} %<extra></extra>")

fig.update_layout(height=600, hovermode="x unified", legend_title_text="Data Type")

fig.show()
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]
In [41]:
# make binary timeseries: above baseline
df["above_baseline"] = (df["state"] > df["baseline_humidity"]).astype(int)

import plotly.graph_objects as go

fig = go.Figure()

# Add smoothed humidity (primary y-axis)
fig.add_trace(go.Scatter(x=df["time"], y=df["state"], mode="lines", name="Smoothed", yaxis="y1"))
fig.add_trace(go.Scatter(x=df["time"], y=df["baseline_humidity"], mode="lines", name="Baseline", yaxis="y1"))

# Add above baseline (secondary y-axis)
fig.add_trace(go.Scatter(x=df["time"], y=df["above_baseline"], mode="lines", name="Above Baseline", yaxis="y2"))

fig.update_layout(
    title=f"Humidity - Smoothed vs Baseline (window={window_size})",
    xaxis=dict(title="Time"),
    yaxis=dict(title="Humidity (%)", side="left"),
    yaxis2=dict(title="Above Baseline", overlaying="y", side="right", range=[-0.05, 1.05]),
    legend_title_text="Data Type",
    height=600,
    hovermode="x unified"
    )

fig.update_traces(hovertemplate="<b>Time:</b> %{x}<br><b>Value:</b> %{y}<extra></extra>")

fig.show()
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]
In [42]:
# Get occupancy data
import pandas as pd
from datetime import datetime
from zoneinfo import ZoneInfo

from utils.database import get_state

local_tz = ZoneInfo("Europe/Copenhagen")

df_occupancy = get_state(
    entity_id="binary_sensor.0xd44867fffe49155d_occupancy",
    # limit=100,
    start_time=datetime(2026, 1, 22, 0, 0, tzinfo=local_tz),
    end_time=datetime(2026, 1, 25, 20, 0, tzinfo=local_tz),
)

# Convert time to Copenhagen timezone
df_occupancy["time"] = df_occupancy["time"].dt.tz_convert(local_tz)

# Keep only states 'on' and 'off'
df_occupancy = df_occupancy[df_occupancy["state"].isin(["on", "off"])].copy()

# Convert state to numeric: 'on'->1, 'off'->0
df_occupancy["state"] = df_occupancy["state"].map({"on": 1, "off": 0})

# Reverse dataframe for chronological order
df_occupancy = df_occupancy.iloc[::-1].reset_index(drop=True)

# Resample to 1 minute intervals, forward fill to propagate last known state
df_occupancy = df_occupancy.set_index("time").resample("1min").ffill().reset_index()

# Make every detected occupancy last for 10 minutes
on_indices = df_occupancy.index[df_occupancy["state"] == 1].tolist()
for idx in on_indices:
    end_idx = min(idx + 10, len(df_occupancy))
    df_occupancy.loc[idx:end_idx - 1, "state"] = 1

df_occupancy.info()
<class 'pandas.DataFrame'>
RangeIndex: 4106 entries, 0 to 4105
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype                            
---  ------  --------------  -----                            
 0   time    4106 non-null   datetime64[ns, Europe/Copenhagen]
 1   state   4105 non-null   float64                          
dtypes: datetime64[ns, Europe/Copenhagen](1), float64(1)
memory usage: 64.3 KB
In [43]:
import plotly.express as px

# Create interactive line plot
fig = px.line(
    df_occupancy,
    x="time",
    y="state",
    title="Occupancy Over Time",
    labels={"time": "Time", "state": "Occupancy"},
)

# Customize hover template
fig.update_traces(hovertemplate="<b>Time:</b> %{x}<br><b>Occupancy:</b> %{y}<extra></extra>")
fig.update_layout(height=600, hovermode="x unified")

fig.show()
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]
In [44]:
# Plot occupancy and above baseline humidity on dual y-axes
import plotly.graph_objects as go

fig = go.Figure()

# Add occupancy state (primary y-axis)
fig.add_trace(go.Scatter(x=df_occupancy["time"], y=df_occupancy["state"], mode="lines", name="Occupancy", yaxis="y1"))

# Add above baseline humidity (secondary y-axis)
fig.add_trace(go.Scatter(x=df["time"], y=df["above_baseline"], mode="lines", name="Above Baseline Humidity", yaxis="y2"))

fig.update_layout(
    title="Occupancy and Above Baseline Humidity Over Time",
    xaxis=dict(title="Time"),
    yaxis=dict(title="Occupancy", side="left", range=[-0.05, 1.05]),
    yaxis2=dict(title="Above Baseline Humidity", overlaying="y", side="right", range=[-0.05, 1.05]),
    legend_title_text="Data Type",
    height=600,
    hovermode="x unified"
    )

fig.update_traces(hovertemplate="<b>Time:</b> %{x}<br><b>Value:</b> %{y}<extra></extra>")

fig.show()
[Data output - unsupported data type map[string]interface {} for mime type application/vnd.plotly.v1+json]

Conclusion

Helpers

Large Toilet Humidity Baseline (last 24h)

Large Toilet Recently Occupied (detected within last 15 minutes)

Start Large Toilet Fan

Large Toilet Recently Occupied OR Large Toilet Humidity Above Large Toilet Humidity Baseline

Stop Large Toilet Fan

Not Large Toilet Recently Occupied AND Large Toilet Humidity Below Large Toilet Humidity Baseline