diff --git a/CHANGELOG.md b/CHANGELOG.md index 9643463..dd7420b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/custom_components/gazdebordeaux/config_flow.py b/custom_components/gazdebordeaux/config_flow.py index 24d0394..a96ecef 100644 --- a/custom_components/gazdebordeaux/config_flow.py +++ b/custom_components/gazdebordeaux/config_flow.py @@ -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__) @@ -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: @@ -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) \ No newline at end of file diff --git a/custom_components/gazdebordeaux/const.py b/custom_components/gazdebordeaux/const.py index a4b301d..af70642 100644 --- a/custom_components/gazdebordeaux/const.py +++ b/custom_components/gazdebordeaux/const.py @@ -1,3 +1,4 @@ """Constants for the Gaz de Bordeaux integration.""" DOMAIN = "gazdebordeaux" +RESET_STATISTICS = "reset_stats" diff --git a/custom_components/gazdebordeaux/coordinator.py b/custom_components/gazdebordeaux/coordinator.py index 9081175..e62cb63 100644 --- a/custom_components/gazdebordeaux/coordinator.py +++ b/custom_components/gazdebordeaux/coordinator.py @@ -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 @@ -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: @@ -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 @@ -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() ) @@ -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") @@ -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 = [] @@ -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 @@ -201,7 +227,8 @@ 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 @@ -209,6 +236,7 @@ async def _async_get_all_data(self) -> list[DailyUsageRead]: 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(), ) \ No newline at end of file diff --git a/custom_components/gazdebordeaux/gazdebordeaux.py b/custom_components/gazdebordeaux/gazdebordeaux.py index bc0e6d2..5aa5861 100644 --- a/custom_components/gazdebordeaux/gazdebordeaux.py +++ b/custom_components/gazdebordeaux/gazdebordeaux.py @@ -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"] @@ -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] = [] @@ -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 diff --git a/custom_components/gazdebordeaux/manifest.json b/custom_components/gazdebordeaux/manifest.json index 0604fb0..b1dcdc6 100644 --- a/custom_components/gazdebordeaux/manifest.json +++ b/custom_components/gazdebordeaux/manifest.json @@ -11,5 +11,5 @@ "issue_tracker": "https://github.com/chriscamicas/gazdebordeaux-ha/issues", "requirements": [ ], - "version": "1.0.0" + "version": "1.1.0" } diff --git a/custom_components/gazdebordeaux/option_flow.py b/custom_components/gazdebordeaux/option_flow.py new file mode 100644 index 0000000..e406e3d --- /dev/null +++ b/custom_components/gazdebordeaux/option_flow.py @@ -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) \ No newline at end of file diff --git a/custom_components/gazdebordeaux/sensor.py b/custom_components/gazdebordeaux/sensor.py index 4906f7e..44366be 100644 --- a/custom_components/gazdebordeaux/sensor.py +++ b/custom_components/gazdebordeaux/sensor.py @@ -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 diff --git a/custom_components/gazdebordeaux/strings.json b/custom_components/gazdebordeaux/strings.json index 58fee87..111d4f3 100644 --- a/custom_components/gazdebordeaux/strings.json +++ b/custom_components/gazdebordeaux/strings.json @@ -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" + } + } + } } } \ No newline at end of file