Skip to content

Commit

Permalink
v1.1.0 fix energy dashboard incorrect values
Browse files Browse the repository at this point in the history
  • Loading branch information
chriscamicas committed May 19, 2024
1 parent 9f3a46b commit 36a8f28
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 27 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2024-05-19
- Fix incorrect values in Energy Dashboard

## [1.0.0] - 2023-11-27
18 changes: 12 additions & 6 deletions custom_components/gazdebordeaux/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@

import voluptuous as vol

from homeassistant import config_entries
from homeassistant.config_entries import ConfigFlow, ConfigEntry, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_create_clientsession

from .const import DOMAIN
from .gazdebordeaux import Gazdebordeaux
from .option_flow import GazdebordeauxOptionFlow

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -43,19 +43,19 @@ async def _validate_login(
return errors


class GazdebordeauxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class GazdebordeauxConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Gazdebordeaux."""

VERSION = 1

def __init__(self) -> None:
"""Initialize a new GazdebordeauxConfigFlow."""
self.reauth_entry: config_entries.ConfigEntry | None = None
self.reauth_entry: ConfigEntry | None = None
self.utility_info: dict[str, Any] | None = None

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
Expand All @@ -70,9 +70,15 @@ async def async_step_user(


@callback
def _async_create_gazdebordeaux_entry(self, data: dict[str, Any]) -> FlowResult:
def _async_create_gazdebordeaux_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
"""Create the config entry."""
return self.async_create_entry(
title=f"({data[CONF_USERNAME]})",
data=data,
)

@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry):
"""Get options flow for this handler"""
return GazdebordeauxOptionFlow(config_entry)
1 change: 1 addition & 0 deletions custom_components/gazdebordeaux/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Constants for the Gaz de Bordeaux integration."""

DOMAIN = "gazdebordeaux"
RESET_STATISTICS = "reset_stats"
56 changes: 42 additions & 14 deletions custom_components/gazdebordeaux/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
from types import MappingProxyType
from typing import Any, cast

from .const import RESET_STATISTICS
from .gazdebordeaux import Gazdebordeaux, DailyUsageRead, TotalUsageRead

from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.util import get_instance
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
statistics_during_period,
StatisticsRow
statistics_during_period
)
from homeassistant.config_entries import ConfigEntries, ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
Expand Down Expand Up @@ -47,6 +48,19 @@ def __init__(
entry_data[CONF_USERNAME],
entry_data[CONF_PASSWORD],
)
self.reset = False
if RESET_STATISTICS in entry_data:
_LOGGER.debug("Asked to reset all statistics...")
self.reset = bool(entry_data[RESET_STATISTICS])
entries=self.hass.config_entries.async_entries(DOMAIN)
_LOGGER.debug("Updating config...")
self.hass.config_entries.async_update_entry(
entries[0], data={
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
RESET_STATISTICS: False
}
)

@callback
def _dummy_listener() -> None:
Expand Down Expand Up @@ -76,9 +90,6 @@ async def _async_update_data(
# we need to insert data into statistics.
await self._insert_statistics()

# Because Opower provides historical usage/cost with a delay of a couple of days
# we need to insert data into statistics.
await self._insert_statistics()
return totalUsage


Expand All @@ -94,6 +105,9 @@ async def _insert_statistics(self) -> None:
volume_statistic_id
)

if self.reset:
_LOGGER.debug("Resetting all statistics...")

last_stat = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, consumption_statistic_id, True, set()
)
Expand All @@ -103,10 +117,13 @@ async def _insert_statistics(self) -> None:
cost_sum = 0.0
consumption_sum = 0.0
volume_sum = 0.0
last_stats_time = None
last_stat_ts = None
else:
last_stat_ts = last_stat[consumption_statistic_id][0]["start"] # type: ignore
last_stat_date = datetime.fromtimestamp(last_stat_ts)
_LOGGER.debug("Last stat found for %s...", last_stat_date.strftime("%Y-%m-%d"))
usage_reads = await self._async_get_recent_usage_reads(
last_stat[consumption_statistic_id][0]["start"] # type: ignore
last_stat_ts
)
if not usage_reads:
_LOGGER.debug("No recent usage/cost data. Skipping update")
Expand All @@ -120,14 +137,14 @@ async def _insert_statistics(self) -> None:
{cost_statistic_id, consumption_statistic_id, volume_statistic_id},
"day",
None,
{"sum"},
{"sate", "sum"},
)
# s:StatisticsRow =stats[cost_statistic_id][0]

cost_sum = cast(float, stats[cost_statistic_id][0]["sum"]) # type: ignore
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"]) # type: ignore
volume_sum = cast(float, stats[volume_statistic_id][0]["sum"]) # type: ignore
last_stats_time = stats[cost_statistic_id][0]["start"] # type: ignore
# last_stat_ts = stats[cost_statistic_id][0]["start"] # type: ignore

cost_statistics = []
consumption_statistics = []
Expand All @@ -136,8 +153,17 @@ async def _insert_statistics(self) -> None:
for usage_read in usage_reads:
start = usage_read.date
start.tzinfo
if last_stats_time is not None and start.timestamp() <= last_stats_time:
continue
if last_stat_ts is not None:
if start.timestamp() <= last_stat_ts:
_LOGGER.debug("Skipping data for %s (timestamp)", start.strftime("%Y-%m-%d"))
continue
# if we are on the same day, skip it as well regarding of the time (to prevent multiple run for the same day)
if start.date() == last_stat_date.date():
_LOGGER.debug("Skipping data for %s (same date)", start.strftime("%Y-%m-%d"))
continue

_LOGGER.debug("Importing data for %s...", start.strftime("%Y-%m-%d"))

cost_sum += usage_read.price
consumption_sum += usage_read.amountOfEnergy
volume_sum += usage_read.volumeOfEnergy
Expand Down Expand Up @@ -201,14 +227,16 @@ async def _async_get_all_data(self) -> list[DailyUsageRead]:
"""
usage_reads = []

start = None
# if start=None it will only default to beginning of current year, let's import 1 year more
start = datetime(datetime.today().year-1, 1, 1)
end = datetime.now()
usage_reads = await self.api.async_get_daily_usage(start, end)
return usage_reads

async def _async_get_recent_usage_reads(self, last_stat_time: float) -> list[DailyUsageRead]:
"""Get cost reads within the past 30 days to allow corrections in data from utilities."""
return await self.api.async_get_daily_usage(
datetime.fromtimestamp(last_stat_time) - timedelta(days=30),
# datetime.fromtimestamp(last_stat_time) - timedelta(days=30),
datetime.fromtimestamp(last_stat_time),
datetime.now(),
)
6 changes: 3 additions & 3 deletions custom_components/gazdebordeaux/gazdebordeaux.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async def async_login(self):
# ------------------------------------------------------
async def async_get_total_usage(self):
monthly_data = await self.async_get_data(None, None, "year")
Logger.debug("Data retreived %s", monthly_data)
# Logger.debug("Data retreived %s", monthly_data)

paris_tz = pytz.timezone('Europe/Paris')
d = monthly_data["total"]
Expand All @@ -66,7 +66,7 @@ async def async_get_total_usage(self):

async def async_get_daily_usage(self, start: datetime|None, end: datetime|None) -> List[DailyUsageRead]:
daily_data = await self.async_get_data(start, end, "month")
Logger.debug("Data retreived %s", daily_data)
# Logger.debug("Data retreived %s", daily_data)

usageReads: List[DailyUsageRead] = []

Expand All @@ -84,7 +84,7 @@ async def async_get_daily_usage(self, start: datetime|None, end: datetime|None)

))

Logger.debug("Data transformed: %s", usageReads)
# Logger.debug("Data transformed: %s", usageReads)
return usageReads


Expand Down
2 changes: 1 addition & 1 deletion custom_components/gazdebordeaux/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@
"issue_tracker": "https://github.com/chriscamicas/gazdebordeaux-ha/issues",
"requirements": [
],
"version": "1.0.0"
"version": "1.1.0"
}
96 changes: 96 additions & 0 deletions custom_components/gazdebordeaux/option_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Config flow for Gazdebordeaux integration."""
from __future__ import annotations

from collections.abc import Mapping
import logging
from typing import Any

import voluptuous as vol

from homeassistant.config_entries import OptionsFlow, ConfigEntry, ConfigFlowResult
from homeassistant.helpers import selector
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.components.sensor.const import DOMAIN as SENSOR_DOMAIN

from .const import DOMAIN, RESET_STATISTICS
from .gazdebordeaux import Gazdebordeaux

_LOGGER = logging.getLogger(__name__)

async def _validate_login(
hass: HomeAssistant, login_data: dict[str, str]
) -> dict[str, str]:
"""Validate login data and return any errors."""
api = Gazdebordeaux(
async_create_clientsession(hass),
login_data[CONF_USERNAME],
login_data[CONF_PASSWORD],
)
errors: dict[str, str] = {}
try:
await api.async_login()
except Exception:
errors["base"] = "invalid_auth"
return errors


class GazdebordeauxOptionFlow(OptionsFlow):
"""Handle a config flow for Gazdebordeaux."""

VERSION = 1
_user_inputs: dict = {}

def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry

async def async_step_init(self, user_input: dict | None = None) -> ConfigFlowResult:
"""Gestion de l'étape 'init'. Point d'entrée de notre
optionsFlow. Comme pour le ConfigFlow, cette méthode est appelée 2 fois
"""

reset_stats: Any = False
if RESET_STATISTICS in self.config_entry.data:
reset_stats = self.config_entry.data[RESET_STATISTICS]

option_form = vol.Schema(
{
vol.Required(CONF_USERNAME, default=self.config_entry.data[CONF_USERNAME]): str,
vol.Required(CONF_PASSWORD, default=self.config_entry.data[CONF_PASSWORD]): str,
vol.Optional(RESET_STATISTICS, default=reset_stats): bool,
}
)

if user_input is None:
_LOGGER.debug(
"option_flow step user (1). 1er appel : pas de user_input -> "
"on affiche le form user_form"
)
return self.async_show_form(step_id="init", data_schema=option_form)

# 2ème appel : il y a des user_input -> on stocke le résultat
_LOGGER.debug(
"option_flow step user (2). On a reçu les valeurs: %s", user_input
)
# On mémorise les user_input
self._user_inputs.update(user_input)

# On appelle le step de fin pour enregistrer les modifications
return await self.async_end()

async def async_end(self):
"""Finalization of the ConfigEntry creation"""
_LOGGER.info(
"Recreation de l'entry %s. La nouvelle config est maintenant : %s",
self.config_entry.entry_id,
self._user_inputs,
)

# Modification de la configEntry avec nos nouvelles valeurs
self.hass.config_entries.async_update_entry(
self.config_entry, data=self._user_inputs
)
# On ne fait rien dans l'objet options dans la configEntry
return self.async_create_entry(title=None, data=None)
8 changes: 5 additions & 3 deletions custom_components/gazdebordeaux/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

from .gazdebordeaux import TotalUsageRead

from homeassistant.components.sensor import (
from homeassistant.components.sensor.const import (
SensorDeviceClass,
SensorStateClass
)
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
SensorEntityDescription
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfVolume
Expand Down
13 changes: 13 additions & 0 deletions custom_components/gazdebordeaux/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,18 @@
}
}
}
},
"options": {
"step": {
"init": {
"title": "Config. existante",
"description": "Modifiez éventuellement la configuration",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"reset_stats": "Efface tout l'historique de statistiques"
}
}
}
}
}

0 comments on commit 36a8f28

Please sign in to comment.