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
10 changes: 9 additions & 1 deletion nuheat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
from nuheat.api import NuHeat
from nuheat.thermostat import NuHeatThermostat
from nuheat.thermostat import (
NuHeatThermostat,
EnergyUsage,
HourlyUsage,
DailyUsage,
WeeklyUsage,
MonthlyUsage,
YearlyUsage,
)
import nuheat.config as config
import nuheat.util as util
276 changes: 275 additions & 1 deletion nuheat/thermostat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from datetime import datetime, timezone, timedelta, time
from dataclasses import dataclass
from datetime import datetime, timezone, timedelta, time, date
from typing import Optional
import nuheat.config as config
from nuheat.util import (
celsius_to_nuheat,
Expand All @@ -8,6 +10,64 @@
)


@dataclass
class HourlyUsage:
"""Hourly energy usage data."""

hour: int
heating_minutes: int
energy_kwh: Optional[float]


@dataclass
class DailyUsage:
"""Daily energy usage data."""

date: date
heating_minutes: int
energy_kwh: Optional[float]


@dataclass
class EnergyUsage:
"""Energy usage data from the NuHeat API (daily view with hourly breakdown)."""

date: date
heating_minutes: int
energy_kwh: Optional[float]
hourly: list


@dataclass
class WeeklyUsage:
"""Weekly energy usage data from the NuHeat API (weekly view with daily breakdown)."""

week_start: date
heating_minutes: int
energy_kwh: Optional[float]
daily: list


@dataclass
class MonthlyUsage:
"""Monthly energy usage data."""

year: int
month: int
heating_minutes: int
energy_kwh: Optional[float]


@dataclass
class YearlyUsage:
"""Yearly energy usage data from the NuHeat API (yearly view with monthly breakdown)."""

year: int
heating_minutes: int
energy_kwh: Optional[float]
monthly: list


class NuHeatThermostat(object):
_session = None
_data = None
Expand Down Expand Up @@ -351,3 +411,217 @@ def set_data(self, post_data):
data=post_data,
params=params,
)

def get_energy_usage(self, for_date: Optional[date] = None) -> EnergyUsage:
"""
Fetch energy usage data for a specific date.

:param for_date: The date to fetch energy usage for. Defaults to today.
:return: EnergyUsage object containing heating minutes and energy in kWh
"""
if for_date is None:
for_date = date.today()

date_str = for_date.strftime("%Y-%m-%d")

params = {
"serialnumber": self.serial_number,
"view": "day",
"date": date_str,
"history": "0",
"calc": "yes",
"weekstart": "sunday",
}

data = self._session.request(
url=f"{self._session._api_url}/energyusage",
params=params,
)

total_minutes = 0
total_kwh = 0.0
has_kwh = False
hourly_data = []

# Response format: {"EnergyUsage": [{"Usage": [{"Minutes": x, "EnergyKWattHour": y}, ...]}]}
energy_usage = data.get("EnergyUsage", [])
for day_data in energy_usage:
usage_entries = day_data.get("Usage", [])
for hour, entry in enumerate(usage_entries):
minutes = entry.get("Minutes", 0)
kwh = entry.get("EnergyKWattHour", 0)

total_minutes += minutes
if kwh > 0:
has_kwh = True
total_kwh += kwh

hourly_data.append(HourlyUsage(
hour=hour,
heating_minutes=minutes,
energy_kwh=kwh if kwh > 0 else None,
))

# If we have heating minutes but no kWh data, return None (user hasn't
# configured watt density in NuHeat app). If heating minutes is 0,
# return 0.0 since zero heating = zero energy.
if total_minutes == 0:
final_kwh: Optional[float] = 0.0
elif has_kwh:
final_kwh = total_kwh
else:
final_kwh = None

return EnergyUsage(
date=for_date,
heating_minutes=total_minutes,
energy_kwh=final_kwh,
hourly=hourly_data,
)

def get_weekly_usage(self, for_date: Optional[date] = None) -> WeeklyUsage:
"""
Fetch weekly energy usage data with daily breakdown.

:param for_date: A date within the week to fetch. Defaults to today.
:return: WeeklyUsage object containing daily breakdown
"""
if for_date is None:
for_date = date.today()

date_str = for_date.strftime("%Y-%m-%d")

params = {
"serialnumber": self.serial_number,
"view": "week",
"date": date_str,
"history": "0",
"calc": "yes",
"weekstart": "sunday",
}

data = self._session.request(
url=f"{self._session._api_url}/energyusage",
params=params,
)

total_minutes = 0
total_kwh = 0.0
has_kwh = False
daily_data = []

# Determine week start based on API response
monday_first = data.get("MondayIsFirstDay", False)

# Calculate the start of the week containing for_date
weekday = for_date.weekday() # Monday=0, Sunday=6
if monday_first:
days_since_start = weekday
else:
# Sunday=0 for week start
days_since_start = (weekday + 1) % 7
week_start = for_date - timedelta(days=days_since_start)

# Response format: {"EnergyUsage": [{"Usage": [{"Minutes": x, "EnergyKWattHour": y}, ...]}]}
energy_usage = data.get("EnergyUsage", [])
for day_data in energy_usage:
usage_entries = day_data.get("Usage", [])
for day_offset, entry in enumerate(usage_entries):
minutes = entry.get("Minutes", 0)
kwh = entry.get("EnergyKWattHour", 0)

total_minutes += minutes
if kwh > 0:
has_kwh = True
total_kwh += kwh

entry_date = week_start + timedelta(days=day_offset)
daily_data.append(DailyUsage(
date=entry_date,
heating_minutes=minutes,
energy_kwh=kwh if kwh > 0 else None,
))

# If we have heating minutes but no kWh data, return None (user hasn't
# configured watt density in NuHeat app). If heating minutes is 0,
# return 0.0 since zero heating = zero energy.
if total_minutes == 0:
final_kwh: Optional[float] = 0.0
elif has_kwh:
final_kwh = total_kwh
else:
final_kwh = None

return WeeklyUsage(
week_start=week_start,
heating_minutes=total_minutes,
energy_kwh=final_kwh,
daily=daily_data,
)

def get_yearly_usage(self, year: Optional[int] = None) -> YearlyUsage:
"""
Fetch yearly energy usage data with monthly breakdown.

:param year: The year to fetch. Defaults to current year.
:return: YearlyUsage object containing monthly breakdown
"""
if year is None:
year = date.today().year

params = {
"serialnumber": self.serial_number,
"view": "year",
"date": str(year),
"history": "0",
"calc": "yes",
"weekstart": "sunday",
}

data = self._session.request(
url=f"{self._session._api_url}/energyusage",
params=params,
)

total_minutes = 0
total_kwh = 0.0
has_kwh = False
monthly_data = []

# Response format: {"EnergyUsage": [{"Usage": [{"Minutes": x, "EnergyKWattHour": y}, ...]}]}
# 12 entries, one per month (January=0 to December=11)
energy_usage = data.get("EnergyUsage", [])
for year_data in energy_usage:
usage_entries = year_data.get("Usage", [])
for month_index, entry in enumerate(usage_entries):
minutes = entry.get("Minutes", 0)
kwh = entry.get("EnergyKWattHour", 0)

total_minutes += minutes
if kwh > 0:
has_kwh = True
total_kwh += kwh

monthly_data.append(MonthlyUsage(
year=year,
month=month_index + 1, # 1-12
heating_minutes=minutes,
energy_kwh=kwh if kwh > 0 else None,
))

# If we have heating minutes but no kWh data, return None (user hasn't
# configured watt density in NuHeat app). If heating minutes is 0,
# return 0.0 since zero heating = zero energy.
if total_minutes == 0:
final_kwh: Optional[float] = 0.0
elif has_kwh:
final_kwh = total_kwh
else:
final_kwh = None

return YearlyUsage(
year=year,
heating_minutes=total_minutes,
energy_kwh=final_kwh,
monthly=monthly_data,
)
11 changes: 11 additions & 0 deletions tests/fixtures/energy_usage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"EnergyUsage": [
{
"Usage": [
{"Minutes": 15, "EnergyKWattHour": 0.3},
{"Minutes": 30, "EnergyKWattHour": 0.6},
{"Minutes": 10, "EnergyKWattHour": 0.2}
]
}
]
}
11 changes: 11 additions & 0 deletions tests/fixtures/energy_usage_no_kwh.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"EnergyUsage": [
{
"Usage": [
{"Minutes": 15, "EnergyKWattHour": 0},
{"Minutes": 30, "EnergyKWattHour": 0},
{"Minutes": 10, "EnergyKWattHour": 0}
]
}
]
}
16 changes: 16 additions & 0 deletions tests/fixtures/weekly_usage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"MondayIsFirstDay": false,
"EnergyUsage": [
{
"Usage": [
{"Minutes": 0, "EnergyKWattHour": 0},
{"Minutes": 0, "EnergyKWattHour": 0},
{"Minutes": 0, "EnergyKWattHour": 0},
{"Minutes": 0, "EnergyKWattHour": 0},
{"Minutes": 0, "EnergyKWattHour": 0},
{"Minutes": 261, "EnergyKWattHour": 0},
{"Minutes": 576, "EnergyKWattHour": 0}
]
}
]
}
21 changes: 21 additions & 0 deletions tests/fixtures/yearly_usage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"MondayIsFirstDay": false,
"EnergyUsage": [
{
"Usage": [
{"Minutes": 13780, "EnergyKWattHour": 0},
{"Minutes": 15400, "EnergyKWattHour": 0},
{"Minutes": 10348, "EnergyKWattHour": 0},
{"Minutes": 9106, "EnergyKWattHour": 0},
{"Minutes": 6233, "EnergyKWattHour": 0},
{"Minutes": 7871, "EnergyKWattHour": 0},
{"Minutes": 9350, "EnergyKWattHour": 0},
{"Minutes": 14239, "EnergyKWattHour": 0},
{"Minutes": 9981, "EnergyKWattHour": 0},
{"Minutes": 15947, "EnergyKWattHour": 0},
{"Minutes": 14183, "EnergyKWattHour": 0},
{"Minutes": 14907, "EnergyKWattHour": 0}
]
}
]
}
Loading