From aa5b4b743da13632f2937582be3b8e22e24966a2 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Sat, 13 Jan 2024 19:44:47 +0100 Subject: [PATCH 1/7] Add support for business accounts --- README.md | 11 +- .../tauron_amiplus/config_flow.py | 5 +- custom_components/tauron_amiplus/connector.py | 119 ++++++++++++++---- custom_components/tauron_amiplus/const.py | 32 +++-- .../tauron_amiplus/coordinator.py | 9 +- custom_components/tauron_amiplus/sensor.py | 40 ++++-- .../tauron_amiplus/statistics.py | 14 ++- 7 files changed, 174 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 6cb9ce8..be578b5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![HACS Default][hacs_shield]][hacs] [![GitHub Latest Release][releases_shield]][latest_release] -[![GitHub All Releases][downloads_total_shield]][releases] +[![GitHub All Releases][downloads_total_shield]][releases] +[![Installations][installations_shield]][releases] [![Ko-Fi][ko_fi_shield]][ko_fi] [![buycoffee.to][buycoffee_to_shield]][buycoffee_to] [![PayPal.Me][paypal_me_shield]][paypal_me] @@ -18,6 +19,8 @@ [releases]: https://github.com/PiotrMachowski/Home-Assistant-custom-components-Tauron-AMIplus/releases [downloads_total_shield]: https://img.shields.io/github/downloads/PiotrMachowski/Home-Assistant-custom-components-Tauron-AMIplus/total +[installations_shield]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fanalytics.home-assistant.io%2Fcustom_integrations.json&query=%24.tauron_amiplus.total&style=popout&color=41bdf5&label=analytics + # Tauron AMIplus sensor @@ -37,7 +40,7 @@ You can also use following [My Home Assistant](http://my.home-assistant.io/) lin
-Warning: yaml configuration is not recommended +Warning: yaml configuration is no longer recommended **Warning:** Not all features are available when using yaml configuration @@ -108,6 +111,8 @@ sensor: ### Using [HACS](https://hacs.xyz/) (recommended) +[![Open your Home Assistant instance and open a repository inside the Home Assistant Community Store.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=PiotrMachowski&repository=Home-Assistant-custom-components-Tauron-AMIplus&category=integration) + * In _Integrations_ section add repository "Tauron AMIplus" * Install added repository @@ -128,7 +133,7 @@ Then restart Home Assistant before applying configuration file changes. * **How to display hourly data in Energy dashboard?** - To show hourly data in Energy dashboard you have to use `tauron_importer` positions. + To show hourly data in Energy dashboard you have to use `tauron_importer` statistics instead of entities. * **Why there are missing days in statistics/Energy dashboard?** diff --git a/custom_components/tauron_amiplus/config_flow.py b/custom_components/tauron_amiplus/config_flow.py index d29d200..414c567 100644 --- a/custom_components/tauron_amiplus/config_flow.py +++ b/custom_components/tauron_amiplus/config_flow.py @@ -187,7 +187,8 @@ def get_schema_init(user_input=None): def get_schema_select_meter(self, user_input=None): if user_input is None: user_input = {} - meter_options = list(map(lambda m: {"label": m["meter_name"], "value": m["meter_id"]}, self._meters)) + meter_options = list( + map(lambda m: {"label": f"({m['meter_type']}) {m['meter_name']}", "value": m["meter_id"]}, self._meters)) data_schema = vol.Schema({ vol.Required(CONF_METER_ID, default=user_input.get(CONF_METER_ID, vol.UNDEFINED)): selector( @@ -227,7 +228,7 @@ def async_get_options_flow(config_entry): class TauronAmiplusOptionsFlowHandler(config_entries.OptionsFlow): - """Blueprint config flow options handler.""" + """Tauron Amiplus config flow options handler.""" def __init__(self, config_entry): """Initialize HACS options flow.""" diff --git a/custom_components/tauron_amiplus/connector.py b/custom_components/tauron_amiplus/connector.py index c12eab6..6325501 100644 --- a/custom_components/tauron_amiplus/connector.py +++ b/custom_components/tauron_amiplus/connector.py @@ -1,16 +1,17 @@ """Update coordinator for TAURON sensors.""" import datetime import logging -import re import ssl +import re from typing import Optional, Tuple import requests -from requests import adapters +from requests import adapters, Response, Session from urllib3 import poolmanager -from .const import (CONST_DATE_FORMAT, CONST_MAX_LOOKUP_RANGE, CONST_REQUEST_HEADERS, CONST_URL_ENERGY, CONST_URL_LOGIN, - CONST_URL_READINGS, CONST_URL_SELECT_METER, CONST_URL_SERVICE) +from .const import (CONST_DATE_FORMAT, CONST_MAX_LOOKUP_RANGE, CONST_REQUEST_HEADERS, CONST_URL_ENERGY, + CONST_URL_ENERGY_BUSINESS, CONST_URL_LOGIN, CONST_URL_LOGIN_MOJ_TAURON, CONST_URL_READINGS, + CONST_URL_SELECT_METER, CONST_URL_SERVICE, CONST_URL_SERVICE_MOJ_TAURON) _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,8 @@ def __init__(self): self.tariff = None self.consumption: Optional[TauronAmiplusDataSet] = None self.generation: Optional[TauronAmiplusDataSet] = None + self.amount_value: Optional[float] = None + self.amount_status: Optional[str] = None def data_unavailable(self): return self.consumption is None or self.generation is None @@ -98,6 +101,7 @@ def __init__(self, username, password, meter_id, show_generation=False, show_12_ self.username = username self.password = password self.meter_id = meter_id + self.is_business = False self.meters = [] self.show_generation = show_generation self.show_12_months = show_12_months @@ -110,6 +114,9 @@ def __init__(self, username, password, meter_id, show_generation=False, show_12_ def get_raw_data(self) -> TauronAmiplusRawData: data = TauronAmiplusRawData() + # amount_value, amount_status = self.get_moj_tauron() + # data.amount_value = amount_value + # data.amount_status = amount_status self.login() generation_max_cache = datetime.datetime.now() data.consumption, consumption_max_cache = self.get_data_set(generation=False) @@ -117,7 +124,7 @@ def get_raw_data(self) -> TauronAmiplusRawData: data.generation, generation_max_cache = self.get_data_set(generation=True) else: data.generation = TauronAmiplusDataSet() - if data.consumption.json_yearly is not None: + if data.consumption.json_yearly is not None and "tariff" in data.consumption.json_yearly["data"]: data.tariff = data.consumption.json_yearly["data"]["tariff"] self._cache.delete_older_than(min(consumption_max_cache, generation_max_cache)) return data @@ -149,53 +156,70 @@ def get_data_set(self, generation) -> Tuple[TauronAmiplusDataSet, datetime.datet cache_max = potential_max return dataset, cache_max - def login(self): + def login_service(self, login_url: str, service: str) -> tuple[Session, Response]: payload_login = { "username": self.username, "password": self.password, - "service": CONST_URL_SERVICE, + "service": service, } session = requests.session() session.mount("https://", TLSAdapter()) - self.log("Logging in...") - session.request( + self.log(f"Logging in... ({service})") + r1 = session.request( "POST", - CONST_URL_LOGIN, + login_url, data=payload_login, headers=CONST_REQUEST_HEADERS, ) + if "Przekroczono maksymalną liczbę logowań." in r1.text: + self.log("Too many login attempts") + raise Exception("Too many login attempts") r2 = session.request( "POST", - CONST_URL_LOGIN, + login_url, data=payload_login, headers=CONST_REQUEST_HEADERS, ) + if "Przekroczono maksymalną liczbę logowań." in r2.text: + self.log("Too many login attempts") + raise Exception("Too many login attempts") if "Login lub hasło są nieprawidłowe." in r2.text: self.log("Invalid credentials") raise Exception("Invalid credentials") - if self.username not in r2.text: + if (self.username not in r2.text) and (self.username.upper() not in r2.text): self.log("Failed to login") raise Exception("Failed to login") + return session, r2 + + def login(self): + session, login_response = self.login_service(CONST_URL_LOGIN, CONST_URL_SERVICE) + self.session = session self.log("Logged in.") - self.meters = self._get_meters(r2.text) + self.meters = self._get_meters(login_response.text) payload_select_meter = {"site[client]": self.meter_id} + selected_meter_info = list(filter(lambda m: m["meter_id"] == self.meter_id, self.meters)) + if len(selected_meter_info) > 0: + self.is_business = selected_meter_info[0]["meter_type"] == "WO" + else: + self.is_business = False self.log(f"Selecting meter: {self.meter_id}") - session.request("POST", CONST_URL_SELECT_METER, data=payload_select_meter, headers=CONST_REQUEST_HEADERS) - self.session = session + self.session.request("POST", CONST_URL_SELECT_METER, data=payload_select_meter, headers=CONST_REQUEST_HEADERS) @staticmethod - def _get_meters(text): + def _get_meters(text: str) -> list: regex = r".*data-data='{\"type\": \".*\"}'>.*" matches = list(re.finditer(regex, text)) meters = [] for match in matches: m1 = re.match(r".*value=\"([\d\_]+)\".*", match.group()) m2 = re.match(r".*\"}'>(.*)", match.group()) - if m1 is None or m2 is None: + m3 = re.match(r".*data-data='{\"type\": \"(.*)\"}'>.*", match.group()) + if m1 is None or m2 is None or m3 is None: continue meter_id = m1.groups()[0] display_name = m2.groups()[0] - meters.append({"meter_id": meter_id, "meter_name": display_name}) + meter_type = m3.groups()[0] + meters.append({"meter_id": meter_id, "meter_name": display_name, "meter_type": meter_type}) return meters def calculate_configuration(self, days_before=2, throw_on_empty=True): @@ -204,10 +228,13 @@ def calculate_configuration(self, days_before=2, throw_on_empty=True): if json_data is None: self.log("Failed to calculate configuration") if throw_on_empty: - raise Exception("Failed to login") + raise Exception("Failed to calculate configuration") else: return None - tariff = json_data["data"]["tariff"] + if "tariff" in json_data["data"]: + tariff = json_data["data"]["tariff"] + else: + tariff = "tariff" self.log(f"Calculated configuration: {tariff}") return tariff @@ -220,6 +247,7 @@ def get_values_yearly(self, generation): "to": TauronAmiplusConnector.format_date(last_day_of_year), "profile": "year", "type": "oze" if generation else "consum", + "energy": 2 if generation else 1, } self.log(f"Downloading yearly data for year: {now.year}, generation: {generation}") values = self.get_chart_values(payload) @@ -240,6 +268,7 @@ def get_values_monthly(self, generation): "to": TauronAmiplusConnector.format_date(last_day_of_month), "profile": "month", "type": "oze" if generation else "consum", + "energy": 2 if generation else 1, } values = self.get_chart_values(payload) if values is not None: @@ -293,7 +322,8 @@ def get_raw_values_daily_for_range(self, day_from: datetime.date, day_to: dateti data["data"]["allData"].extend(day_data["data"]["allData"]) data["data"]["sum"] += day_data["data"]["sum"] data["data"]["zonesName"] = day_data["data"]["zonesName"] - data["data"]["tariff"] = day_data["data"]["tariff"] + if "tariff" in day_data["data"]: + data["data"]["tariff"] = day_data["data"]["tariff"] for z, v in day_data["data"]["zones"].items(): if z in data["data"]["zones"]: data["data"]["zones"][z] += v @@ -316,6 +346,7 @@ def get_raw_values_daily_for_day(self, day, generation): "to": day_str, "profile": "full time", "type": "oze" if generation else "consum", + "energy": 2 if generation else 1, } self.log(f"Downloading daily data for day: {day_str}, generation: {generation}") values = self.get_chart_values(payload) @@ -346,15 +377,20 @@ def get_reading(self, generation): return post def get_chart_values(self, payload): - return self.execute_post(CONST_URL_ENERGY, payload) + return self.execute_post(CONST_URL_ENERGY_BUSINESS if self.is_business else CONST_URL_ENERGY, payload) def execute_post(self, url, payload): + self.log(f"EXECUTING: {url} with payload: {payload}") response = self.session.request( "POST", url, data=payload, headers=CONST_REQUEST_HEADERS, ) + self.log(f"RESPONSE: {response.text}") + if "Przekroczono maksymalną liczbę logowań." in response.text: + self.log("Too many login attempts") + raise Exception("Too many login attempts") if response.status_code == 200 and response.text.startswith('{"success":true'): json_data = response.json() return json_data @@ -363,6 +399,41 @@ def execute_post(self, url, payload): def log(self, msg): _LOGGER.debug(f"[{self.meter_id}]: {msg}") + def get_moj_tauron(self): + session, response = self.login_service(CONST_URL_LOGIN_MOJ_TAURON, CONST_URL_SERVICE_MOJ_TAURON) + + if response is None: + return None, "unknown" + find_1_1 = re.findall(r".*class=\"amount-value\".*", response.text) + find_2_1 = re.findall(r".*class=\"amount-status\".*", response.text) + if len(find_1_1) > 0 and len(find_2_1) > 0: + amount_value = float( + find_1_1[0].strip() + .replace("", "") + .replace("zł", "") + .replace("", "") + .replace(",", ".").strip()) + amount_status = (find_2_1[0].strip() + .replace("", "") + .replace("", "").strip()) + return amount_value, amount_status + + find_1_2 = re.findall(r".*class=\"amount\".*\s*.*\s*", response.text) + find_2_2 = re.findall(r".*class=\"date\".*", response.text) + if len(find_1_2) > 0 and len(find_2_2) > 0: + amount_value = float( + find_1_2[0].strip() + .replace("
", "") + .replace("zł", "") + .replace("
", "") + .replace(",", ".").strip()) + amount_status = (find_2_2[0].strip() + .replace("
", "") + .replace("
", "").strip()) + return amount_value, amount_status + + return None, "unknown" + @staticmethod def format_date(date): return date.strftime(CONST_DATE_FORMAT) @@ -373,7 +444,7 @@ def get_available_meters(username, password): connector.login() if connector.meters is not None and len(connector.meters) > 0: return connector.meters - raise Exception("Failed to login") + raise Exception("Failed to retrieve energy meters") @staticmethod def calculate_tariff(username, password, meter_id): @@ -382,7 +453,7 @@ def calculate_tariff(username, password, meter_id): config = connector.calculate_configuration() if config is not None: return config - raise Exception("Failed to login") + raise Exception("Failed to calculate configuration") @staticmethod def add_all_data(data: dict, date): diff --git a/custom_components/tauron_amiplus/const.py b/custom_components/tauron_amiplus/const.py index 4ab5d06..e3244cc 100644 --- a/custom_components/tauron_amiplus/const.py +++ b/custom_components/tauron_amiplus/const.py @@ -21,9 +21,12 @@ CONST_MAX_LOOKUP_RANGE = 7 CONST_URL_LOGIN = "https://logowanie.tauron-dystrybucja.pl/login" CONST_URL_SERVICE = "https://elicznik.tauron-dystrybucja.pl" +CONST_URL_LOGIN_MOJ_TAURON = "https://logowanie.tauron.pl/login" +CONST_URL_SERVICE_MOJ_TAURON = "https://moj.tauron.pl" CONST_URL_SELECT_METER = f"{CONST_URL_SERVICE}/ustaw_punkt" CONST_URL_ENERGY = f"{CONST_URL_SERVICE}/energia/api" CONST_URL_READINGS = f"{CONST_URL_SERVICE}/odczyty/api" +CONST_URL_ENERGY_BUSINESS = f"{CONST_URL_SERVICE}/energia/wo/api" CONST_REQUEST_HEADERS = {"cache-control": "no-cache"} CONST_CONSUMPTION = "consumption" CONST_GENERATION = "generation" @@ -51,6 +54,9 @@ TYPE_GENERATION_YEARLY = f"{CONST_GENERATION}_{CONST_YEARLY}" TYPE_GENERATION_LAST_12_MONTHS = f"{CONST_GENERATION}_{CONST_LAST_12_MONTHS}" TYPE_GENERATION_CONFIGURABLE = f"{CONST_GENERATION}_{CONST_CONFIGURABLE}" +TYPE_AMOUNT = "moj_tauron" +TYPE_AMOUNT_VALUE = f"{TYPE_AMOUNT}_VALUE" +TYPE_AMOUNT_STATUS = f"{TYPE_AMOUNT}_STATUS" DEFAULT_UPDATE_INTERVAL = timedelta(hours=8, minutes=30) SENSOR_TYPES_YAML = { @@ -64,19 +70,19 @@ }, TYPE_CONSUMPTION_MONTHLY: { "name": "Monthly energy consumption", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_CONSUMPTION_YEARLY: { "name": "Yearly energy consumption", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_CONSUMPTION_LAST_12_MONTHS: { "name": "Last 12 months energy consumption", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_GENERATION_READING: { "name": "Current generation reading", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_GENERATION_DAILY: { "name": "Daily energy generation", @@ -84,15 +90,15 @@ }, TYPE_GENERATION_MONTHLY: { "name": "Monthly energy generation", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_GENERATION_YEARLY: { "name": "Yearly energy generation", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_GENERATION_LAST_12_MONTHS: { "name": "Last 12 months energy generation", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_BALANCED_DAILY: { "name": "Daily balance", @@ -115,14 +121,22 @@ **SENSOR_TYPES_YAML, TYPE_CONSUMPTION_CONFIGURABLE: { "name": "Configurable energy consumption", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_GENERATION_CONFIGURABLE: { "name": "Configurable energy generation", - "state_class": SensorStateClass.TOTAL_INCREASING, + "state_class": SensorStateClass.TOTAL, }, TYPE_BALANCED_CONFIGURABLE: { "name": "Configurable balance", "state_class": SensorStateClass.TOTAL, }, + # TYPE_AMOUNT_VALUE: { + # "name": "Account balance", + # "state_class": SensorStateClass.MEASUREMENT, + # }, + # TYPE_AMOUNT_STATUS: { + # "name": "Account status", + # "state_class": None, + # }, } diff --git a/custom_components/tauron_amiplus/coordinator.py b/custom_components/tauron_amiplus/coordinator.py index 08f0af0..d4fbf1a 100644 --- a/custom_components/tauron_amiplus/coordinator.py +++ b/custom_components/tauron_amiplus/coordinator.py @@ -13,15 +13,16 @@ class TauronAmiplusUpdateCoordinator(DataUpdateCoordinator[TauronAmiplusRawData]): - def __init__(self, hass: HomeAssistant, username, password, meter_id, show_generation=False, show_12_months=False, - show_balanced=False, show_balanced_year=False, show_configurable=False, show_configurable_date=None, - store_statistics=False): + def __init__(self, hass: HomeAssistant, username, password, meter_id, meter_name: str, show_generation=False, + show_12_months=False, show_balanced=False, show_balanced_year=False, show_configurable=False, + show_configurable_date=None, store_statistics=False): super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL, update_method=self.update_method) self.connector = TauronAmiplusConnector(username, password, meter_id, show_generation, show_12_months, show_balanced, show_balanced_year, show_configurable, show_configurable_date) self.meter_id = meter_id + self.meter_name = meter_name self.show_generation = show_generation self.show_12_months = show_12_months self.show_balanced = show_balanced @@ -40,7 +41,7 @@ async def update_method(self) -> TauronAmiplusRawData: return data async def generate_statistics(self, data): - statistics_updater = TauronAmiplusStatisticsUpdater(self.hass, self.connector, self.meter_id, + statistics_updater = TauronAmiplusStatisticsUpdater(self.hass, self.connector, self.meter_id, self.meter_name, self.show_generation, self.show_balanced) await statistics_updater.update_all(data) diff --git a/custom_components/tauron_amiplus/sensor.py b/custom_components/tauron_amiplus/sensor.py index 5572cb1..d9d3449 100644 --- a/custom_components/tauron_amiplus/sensor.py +++ b/custom_components/tauron_amiplus/sensor.py @@ -15,7 +15,8 @@ CONF_TARIFF, CONST_BALANCED, CONST_CONFIGURABLE, CONST_DAILY, CONST_GENERATION, CONST_LAST_12_MONTHS, CONST_MONTHLY, CONST_READING, CONST_URL_SERVICE, CONST_YEARLY, DEFAULT_NAME, DOMAIN, SENSOR_TYPES, SENSOR_TYPES_YAML, TYPE_BALANCED_CONFIGURABLE, TYPE_BALANCED_DAILY, - TYPE_BALANCED_LAST_12_MONTHS, TYPE_BALANCED_MONTHLY, TYPE_BALANCED_YEARLY) + TYPE_BALANCED_LAST_12_MONTHS, TYPE_BALANCED_MONTHLY, TYPE_BALANCED_YEARLY, + TYPE_AMOUNT, TYPE_AMOUNT_STATUS, TYPE_AMOUNT_VALUE) from .coordinator import TauronAmiplusUpdateCoordinator _LOGGER = logging.getLogger(__name__) @@ -41,7 +42,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= show_balanced = any(filter(lambda v: CONST_BALANCED in v, config[CONF_MONITORED_VARIABLES])) show_balanced_year = CONF_SHOW_BALANCED_YEAR in config[CONF_MONITORED_VARIABLES] - coordinator = TauronAmiplusUpdateCoordinator(hass, username, password, meter_id, + coordinator = TauronAmiplusUpdateCoordinator(hass, username, password, meter_id, name, show_generation=show_generation_sensors, show_12_months=show_12_months, show_balanced=show_balanced, @@ -89,7 +90,7 @@ async def async_setup_entry(hass, entry: ConfigEntry, async_add_entities): if not show_configurable: sensor_types = {k: v for k, v in sensor_types.items() if not k.endswith(CONST_CONFIGURABLE)} - coordinator = TauronAmiplusUpdateCoordinator(hass, user, password, meter_id, + coordinator = TauronAmiplusUpdateCoordinator(hass, user, password, meter_id, meter_name, show_generation=show_generation_sensors, show_12_months=show_12_months, show_balanced=show_balanced, @@ -135,10 +136,16 @@ def name(self): @property def native_unit_of_measurement(self): + if self._sensor_type == TYPE_AMOUNT_VALUE: + return "zł" + elif self._sensor_type == TYPE_AMOUNT_STATUS: + return None return ENERGY_KILO_WATT_HOUR @property def device_class(self): + if self._sensor_type.startswith(TYPE_AMOUNT): + return None return SensorDeviceClass.ENERGY @property @@ -168,7 +175,13 @@ def _handle_coordinator_update(self) -> None: return dataset = data.generation if self._generation else data.consumption - if self._sensor_type == TYPE_BALANCED_DAILY and data.balance_daily is not None: + if self._sensor_type == TYPE_AMOUNT_VALUE and data.amount_value is not None: + self._state = data.amount_value + self._params = {"status": data.amount_status} + elif self._sensor_type == TYPE_AMOUNT_STATUS and data.amount_status is not None: + self._state = data.amount_status + self._params = {"value": data.amount_value} + elif self._sensor_type == TYPE_BALANCED_DAILY and data.balance_daily is not None: self.update_balanced_data(data.balance_daily, data.tariff) elif self._sensor_type == TYPE_BALANCED_MONTHLY and data.balance_monthly is not None: self.update_balanced_data(data.balance_monthly, data.tariff) @@ -223,13 +236,24 @@ def update_balanced_data(self, balanced_data, tariff): @staticmethod def get_data_from_json(json_data): total = round(json_data["data"]["sum"], 3) - tariff = json_data["data"]["tariff"] + if "tariff" in json_data["data"]: + tariff = json_data["data"]["tariff"] + else: + tariff = "tariff" zones = {} data_range = None - if len(json_data["data"]["zones"]) > 0: + if ( + "zones" in json_data["data"] + and len(json_data["data"]["zones"]) > 0 + and "zonesName" in json_data["data"] + and len(json_data["data"]["zonesName"]) > 0 + ): zones = {v: round(json_data["data"]["zones"][k], 3) for (k, v) in json_data["data"]["zonesName"].items()} - if "allData" in json_data["data"] and len(json_data["data"]["allData"]) > 0 and "Date" in \ - json_data["data"]["allData"][0]: + if ( + "allData" in json_data["data"] + and len(json_data["data"]["allData"]) > 0 + and "Date" in json_data["data"]["allData"][0] + ): consumption_data = json_data["data"]["allData"] data_range = f"{consumption_data[0]['Date']} - {consumption_data[-1]['Date']}" return total, tariff, zones, data_range diff --git a/custom_components/tauron_amiplus/statistics.py b/custom_components/tauron_amiplus/statistics.py index 378ad28..2c714c0 100644 --- a/custom_components/tauron_amiplus/statistics.py +++ b/custom_components/tauron_amiplus/statistics.py @@ -10,19 +10,20 @@ from homeassistant.util.dt import as_utc, get_time_zone, utc_from_timestamp from .connector import TauronAmiplusConnector, TauronAmiplusRawData -from .const import (CONF_METER_ID, CONF_SHOW_BALANCED, CONF_SHOW_GENERATION, CONST_BALANCED, CONST_CONSUMPTION, - CONST_GENERATION, DEFAULT_NAME, STATISTICS_DOMAIN) +from .const import (CONF_METER_ID, CONF_METER_NAME, CONF_SHOW_BALANCED, CONF_SHOW_GENERATION, CONST_BALANCED, + CONST_CONSUMPTION, CONST_GENERATION, DEFAULT_NAME, STATISTICS_DOMAIN) _LOGGER = logging.getLogger(__name__) class TauronAmiplusStatisticsUpdater: - def __init__(self, hass: HomeAssistant, connector: TauronAmiplusConnector, meter_id: str, show_generation: bool, - show_balanced: bool) -> None: + def __init__(self, hass: HomeAssistant, connector: TauronAmiplusConnector, meter_id: str, meter_name: str, + show_generation: bool, show_balanced: bool) -> None: self.hass = hass self.connector = connector self.meter_id = meter_id + self.meter_name = meter_name self.show_generation = show_generation self.show_balanced = show_balanced @@ -31,13 +32,14 @@ async def manually_update(hass, start_date: datetime.date, entry): username = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] meter_id = entry.data[CONF_METER_ID] + meter_name = entry.data[CONF_METER_NAME] show_generation = entry.options.get(CONF_SHOW_GENERATION, False) show_balanced = entry.options.get(CONF_SHOW_BALANCED, False) connector = TauronAmiplusConnector(username, password, meter_id, show_generation=show_generation, show_balanced=show_balanced) - statistics_updater = TauronAmiplusStatisticsUpdater(hass, connector, meter_id, show_generation, show_balanced) + statistics_updater = TauronAmiplusStatisticsUpdater(hass, connector, meter_id, meter_name, show_generation, show_balanced) data = await hass.async_add_executor_job(connector.get_raw_data) start_date = datetime.datetime.combine(start_date, @@ -152,7 +154,7 @@ def get_stats_id(self, suffix): return f"{STATISTICS_DOMAIN}:{self.meter_id}_{suffix}".lower() def get_stats_name(self, suffix): - return f"{DEFAULT_NAME} {self.meter_id} {suffix}" + return f"{DEFAULT_NAME} {self.meter_name} {suffix}" @staticmethod def are_stats_up_to_date(last_stats_end): From f7004c4b2df11c7410e4b24af7753eb550c116a8 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Sat, 13 Jan 2024 20:23:12 +0100 Subject: [PATCH 2/7] Fix deprecation error --- custom_components/tauron_amiplus/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/tauron_amiplus/sensor.py b/custom_components/tauron_amiplus/sensor.py index d9d3449..3e11729 100644 --- a/custom_components/tauron_amiplus/sensor.py +++ b/custom_components/tauron_amiplus/sensor.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorDeviceClass, SensorEntity, SensorStateClass from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_MONITORED_VARIABLES, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_MONITORED_VARIABLES, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util.dt import parse_date @@ -140,7 +140,7 @@ def native_unit_of_measurement(self): return "zł" elif self._sensor_type == TYPE_AMOUNT_STATUS: return None - return ENERGY_KILO_WATT_HOUR + return UnitOfEnergy.KILO_WATT_HOUR @property def device_class(self): From 72c8f80faf799022e80be268ebc840e05ae98f93 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Sat, 13 Jan 2024 20:23:55 +0100 Subject: [PATCH 3/7] Set version to v2.6.0-beta --- custom_components/tauron_amiplus/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tauron_amiplus/manifest.json b/custom_components/tauron_amiplus/manifest.json index 9393617..6184844 100644 --- a/custom_components/tauron_amiplus/manifest.json +++ b/custom_components/tauron_amiplus/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Tauron-AMIplus/issues", "requirements": ["requests"], - "version": "v2.5.2" + "version": "v2.6.0-beta" } From dc759f9e688bbcc017cc76246b60c4cf7806ef10 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Sat, 13 Jan 2024 20:33:10 +0100 Subject: [PATCH 4/7] Add translations for download_statistics service call --- README.md | 2 +- custom_components/tauron_amiplus/strings.json | 20 +++++++++++++++++-- .../tauron_amiplus/translations/en.json | 16 +++++++++++++++ .../tauron_amiplus/translations/pl.json | 16 +++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index be578b5..4543a60 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This sensor uses unofficial API to get energy usage and generation data from [*T To configure this integration go to: _Configuration_ -> _Integrations_ -> _Add integration_ -> _Tauron AMIplus_. -You can also use following [My Home Assistant](http://my.home-assistant.io/) link +You can also use following [My Home Assistant](http://my.home-assistant.io/) link: [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=tauron_amiplus) diff --git a/custom_components/tauron_amiplus/strings.json b/custom_components/tauron_amiplus/strings.json index 55ee1f3..36c7203 100644 --- a/custom_components/tauron_amiplus/strings.json +++ b/custom_components/tauron_amiplus/strings.json @@ -21,7 +21,7 @@ }, "config_options": { "title": "Configuration", - "description": "Select data that should be downloaded by the integration.\nDo not enable entries that aren't necessary to avoid excessive data loading times.\n\n {error_info} ", + "description": "Select data that should be downloaded by the integration.\nDo not enable entries that aren't necessary to avoid excessive data loading times.", "data": { "energy_meter_name": "Name of the energy meter that should be used in UI", "show_generation_sensors": "Enable downloading energy generation data", @@ -44,7 +44,7 @@ "step": { "user": { "title": "Configuration", - "description": "Select data that should be downloaded by the integration.\nDo not enable entries that aren't necessary to avoid excessive data loading times.\n\n {error_info} ", + "description": "Select data that should be downloaded by the integration.\nDo not enable entries that aren't necessary to avoid excessive data loading times.", "data": { "show_generation_sensors": "Enable downloading energy generation data", "show_balanced_sensors": "Enable calculating balanced energy usage data", @@ -59,5 +59,21 @@ "error": { "missing_configurable_start_date": "Missing start date for configurable sensors" } + }, + "services": { + "download_statistics": { + "name": "Download statistics", + "description": "Downloads statistics for a given meter. Overrides all already downloaded data.", + "fields": { + "device_id": { + "name": "Target device", + "description": "The device to download statistics for." + }, + "start_date": { + "name": "Start date", + "description": "Start date of statistics to download." + } + } + } } } diff --git a/custom_components/tauron_amiplus/translations/en.json b/custom_components/tauron_amiplus/translations/en.json index 737b2fc..36c7203 100644 --- a/custom_components/tauron_amiplus/translations/en.json +++ b/custom_components/tauron_amiplus/translations/en.json @@ -59,5 +59,21 @@ "error": { "missing_configurable_start_date": "Missing start date for configurable sensors" } + }, + "services": { + "download_statistics": { + "name": "Download statistics", + "description": "Downloads statistics for a given meter. Overrides all already downloaded data.", + "fields": { + "device_id": { + "name": "Target device", + "description": "The device to download statistics for." + }, + "start_date": { + "name": "Start date", + "description": "Start date of statistics to download." + } + } + } } } diff --git a/custom_components/tauron_amiplus/translations/pl.json b/custom_components/tauron_amiplus/translations/pl.json index a0acd2d..b2371f4 100644 --- a/custom_components/tauron_amiplus/translations/pl.json +++ b/custom_components/tauron_amiplus/translations/pl.json @@ -58,5 +58,21 @@ "error": { "missing_configurable_start_date": "Nie skonfigurowano początku okresu dla sensorów konfigurowalnych" } + }, + "services": { + "download_statistics": { + "name": "Pobierz statystyki", + "description": "Pobiera dane historyczne dla wybranego urządzenia nadpisując wszystkie dotychczas pobrane wartości.", + "fields": { + "device_id": { + "name": "Docelowe urządzenie", + "description": "Urządzenie dla którego będą pobrane statystyki." + }, + "start_date": { + "name": "Data początkowa.", + "description": "Data od której będą pobrane statystyki." + } + } + } } } From 039749dfe4ae4d81339ab41685f22c4738d9f01b Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Wed, 17 Jan 2024 06:03:52 +0100 Subject: [PATCH 5/7] Change a method of retrieving tariff --- custom_components/tauron_amiplus/connector.py | 15 ++++++---- custom_components/tauron_amiplus/sensor.py | 28 ++++++++----------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/custom_components/tauron_amiplus/connector.py b/custom_components/tauron_amiplus/connector.py index 6325501..99d645e 100644 --- a/custom_components/tauron_amiplus/connector.py +++ b/custom_components/tauron_amiplus/connector.py @@ -117,15 +117,13 @@ def get_raw_data(self) -> TauronAmiplusRawData: # amount_value, amount_status = self.get_moj_tauron() # data.amount_value = amount_value # data.amount_status = amount_status - self.login() + data.tariff = self.login() generation_max_cache = datetime.datetime.now() data.consumption, consumption_max_cache = self.get_data_set(generation=False) if self.show_generation or self.show_balanced: data.generation, generation_max_cache = self.get_data_set(generation=True) else: data.generation = TauronAmiplusDataSet() - if data.consumption.json_yearly is not None and "tariff" in data.consumption.json_yearly["data"]: - data.tariff = data.consumption.json_yearly["data"]["tariff"] self._cache.delete_older_than(min(consumption_max_cache, generation_max_cache)) return data @@ -203,7 +201,12 @@ def login(self): else: self.is_business = False self.log(f"Selecting meter: {self.meter_id}") - self.session.request("POST", CONST_URL_SELECT_METER, data=payload_select_meter, headers=CONST_REQUEST_HEADERS) + select_response = self.session.request("POST", CONST_URL_SELECT_METER, data=payload_select_meter, headers=CONST_REQUEST_HEADERS) + tariff_search = re.findall(r"'Tariff' : '(.*)',", select_response.text) + if len(tariff_search) > 0: + tariff = tariff_search[0] + return tariff + return "unknown" @staticmethod def _get_meters(text: str) -> list: @@ -449,10 +452,10 @@ def get_available_meters(username, password): @staticmethod def calculate_tariff(username, password, meter_id): connector = TauronAmiplusConnector(username, password, meter_id) - connector.login() + tariff = connector.login() config = connector.calculate_configuration() if config is not None: - return config + return tariff raise Exception("Failed to calculate configuration") @staticmethod diff --git a/custom_components/tauron_amiplus/sensor.py b/custom_components/tauron_amiplus/sensor.py index 3e11729..1df0c37 100644 --- a/custom_components/tauron_amiplus/sensor.py +++ b/custom_components/tauron_amiplus/sensor.py @@ -174,6 +174,7 @@ def _handle_coordinator_update(self) -> None: if not self.available or data is None: return dataset = data.generation if self._generation else data.consumption + self._tariff = data.tariff if self._sensor_type == TYPE_AMOUNT_VALUE and data.amount_value is not None: self._state = data.amount_value @@ -182,17 +183,17 @@ def _handle_coordinator_update(self) -> None: self._state = data.amount_status self._params = {"value": data.amount_value} elif self._sensor_type == TYPE_BALANCED_DAILY and data.balance_daily is not None: - self.update_balanced_data(data.balance_daily, data.tariff) + self.update_balanced_data(data.balance_daily) elif self._sensor_type == TYPE_BALANCED_MONTHLY and data.balance_monthly is not None: - self.update_balanced_data(data.balance_monthly, data.tariff) + self.update_balanced_data(data.balance_monthly) elif self._sensor_type == TYPE_BALANCED_YEARLY and data.balance_yearly is not None: - self.update_balanced_data(data.balance_yearly, data.tariff) + self.update_balanced_data(data.balance_yearly) elif self._sensor_type == TYPE_BALANCED_LAST_12_MONTHS and data.balance_last_12_months_hourly is not None: - self.update_balanced_data(data.balance_last_12_months_hourly, data.tariff) + self.update_balanced_data(data.balance_last_12_months_hourly) elif self._sensor_type == TYPE_BALANCED_CONFIGURABLE and data.balance_configurable_hourly is not None: - self.update_balanced_data(data.balance_configurable_hourly, data.tariff) + self.update_balanced_data(data.balance_configurable_hourly) elif self._sensor_type.endswith(CONST_READING) and dataset.json_reading is not None: - self.update_reading(dataset.json_reading, data.tariff) + self.update_reading(dataset.json_reading) elif self._sensor_type.endswith(CONST_DAILY) and dataset.json_daily is not None: self.update_values(dataset.json_daily) self._params = {"date": dataset.daily_date, **self._params} @@ -206,26 +207,23 @@ def _handle_coordinator_update(self) -> None: self.update_values(dataset.json_configurable_hourly) self.async_write_ha_state() - def update_reading(self, json_data, tariff): + def update_reading(self, json_data): reading = json_data["data"][-1] self._state = reading["C"] partials = {s: reading[s] for s in ["S1", "S2", "S3"] if s in reading and reading[s] is not None} self._params = {"date": reading["Date"], **partials} - self._tariff = tariff def update_values(self, json_data): - total, tariff, zones, data_range = TauronAmiplusSensor.get_data_from_json(json_data) + total, zones, data_range = TauronAmiplusSensor.get_data_from_json(json_data) self._state = total - self._tariff = tariff self._params = {**zones, "data_range": data_range} self._params = {k: v for k, v in self._params.items() if v is not None} - def update_balanced_data(self, balanced_data, tariff): + def update_balanced_data(self, balanced_data): con = balanced_data[0] gen = balanced_data[1] balance, sum_consumption, sum_generation, zones, data_range = TauronAmiplusSensor.get_balanced_data(con, gen) self._state = round(balance, 3) - self._tariff = tariff self._params = { "sum_consumption": round(sum_consumption, 3), "sum_generation": round(sum_generation, 3), @@ -236,10 +234,6 @@ def update_balanced_data(self, balanced_data, tariff): @staticmethod def get_data_from_json(json_data): total = round(json_data["data"]["sum"], 3) - if "tariff" in json_data["data"]: - tariff = json_data["data"]["tariff"] - else: - tariff = "tariff" zones = {} data_range = None if ( @@ -256,7 +250,7 @@ def get_data_from_json(json_data): ): consumption_data = json_data["data"]["allData"] data_range = f"{consumption_data[0]['Date']} - {consumption_data[-1]['Date']}" - return total, tariff, zones, data_range + return total, zones, data_range @staticmethod def get_balanced_data(consumption_data_json, generation_data_json): From 4d7269c94732935be464b2e63535a5f06c13be10 Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Fri, 19 Jan 2024 03:07:09 +0100 Subject: [PATCH 6/7] Remove configuration calculation --- .../tauron_amiplus/config_flow.py | 7 ++----- custom_components/tauron_amiplus/connector.py | 19 +------------------ 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/custom_components/tauron_amiplus/config_flow.py b/custom_components/tauron_amiplus/config_flow.py index 414c567..cd50a8d 100644 --- a/custom_components/tauron_amiplus/config_flow.py +++ b/custom_components/tauron_amiplus/config_flow.py @@ -95,18 +95,15 @@ async def async_step_select_meter(self, user_input=None): if len(errors) == 0: try: - tariff = None - calculated = await self.hass.async_add_executor_job( + tariff = await self.hass.async_add_executor_job( TauronAmiplusConnector.calculate_tariff, self._username, self._password, user_input[CONF_METER_ID]) - if calculated is not None: - tariff = calculated if tariff is not None: self._meter_id = user_input[CONF_METER_ID] self._tariff = tariff return await self.async_step_config_options() errors = {CONF_METER_ID: "server_no_connection"} - description_placeholders = {"error_info": str(calculated)} + description_placeholders = {"error_info": str(tariff)} except Exception as e: errors = {CONF_PASSWORD: "server_no_connection"} description_placeholders = {"error_info": str(e)} diff --git a/custom_components/tauron_amiplus/connector.py b/custom_components/tauron_amiplus/connector.py index 99d645e..63cb9c2 100644 --- a/custom_components/tauron_amiplus/connector.py +++ b/custom_components/tauron_amiplus/connector.py @@ -225,22 +225,6 @@ def _get_meters(text: str) -> list: meters.append({"meter_id": meter_id, "meter_name": display_name, "meter_type": meter_type}) return meters - def calculate_configuration(self, days_before=2, throw_on_empty=True): - self.log("Calculating configuration...") - json_data, _ = self.get_raw_values_daily(days_before, generation=False) - if json_data is None: - self.log("Failed to calculate configuration") - if throw_on_empty: - raise Exception("Failed to calculate configuration") - else: - return None - if "tariff" in json_data["data"]: - tariff = json_data["data"]["tariff"] - else: - tariff = "tariff" - self.log(f"Calculated configuration: {tariff}") - return tariff - def get_values_yearly(self, generation): now = datetime.datetime.now() first_day_of_year = now.replace(day=1, month=1) @@ -453,8 +437,7 @@ def get_available_meters(username, password): def calculate_tariff(username, password, meter_id): connector = TauronAmiplusConnector(username, password, meter_id) tariff = connector.login() - config = connector.calculate_configuration() - if config is not None: + if tariff is not None: return tariff raise Exception("Failed to calculate configuration") From 24ab04e0649de3058ef8427c245534fd1579775c Mon Sep 17 00:00:00 2001 From: Piotr Machowski Date: Fri, 19 Jan 2024 03:35:42 +0100 Subject: [PATCH 7/7] Set version to v2.6.0 --- custom_components/tauron_amiplus/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/tauron_amiplus/manifest.json b/custom_components/tauron_amiplus/manifest.json index 6184844..5adcf55 100644 --- a/custom_components/tauron_amiplus/manifest.json +++ b/custom_components/tauron_amiplus/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Tauron-AMIplus/issues", "requirements": ["requests"], - "version": "v2.6.0-beta" + "version": "v2.6.0" }