Skip to content

Commit

Permalink
Merge pull request #176 from PiotrMachowski/dev
Browse files Browse the repository at this point in the history
v2.6.0
  • Loading branch information
PiotrMachowski authored Jan 19, 2024
2 parents 1a65394 + 24ab04e commit 3c58741
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 98 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[![HACS Default][hacs_shield]][hacs]
[![GitHub Latest Release][releases_shield]][latest_release]
[![GitHub All Releases][downloads_total_shield]][releases]<!-- piotrmachowski_support_badges_start -->
[![GitHub All Releases][downloads_total_shield]][releases]
[![Installations][installations_shield]][releases]<!-- piotrmachowski_support_badges_start -->
[![Ko-Fi][ko_fi_shield]][ko_fi]
[![buycoffee.to][buycoffee_to_shield]][buycoffee_to]
[![PayPal.Me][paypal_me_shield]][paypal_me]
Expand All @@ -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

Expand All @@ -29,15 +32,15 @@ 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)

### Manual - yaml


<details>
<summary>Warning: yaml configuration is not recommended</summary>
<summary>Warning: yaml configuration is no longer recommended</summary>


**Warning:** Not all features are available when using yaml configuration
Expand Down Expand Up @@ -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

Expand All @@ -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?**

Expand Down
12 changes: 5 additions & 7 deletions custom_components/tauron_amiplus/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -187,7 +184,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(
Expand Down Expand Up @@ -227,7 +225,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."""
Expand Down
139 changes: 98 additions & 41 deletions custom_components/tauron_amiplus/connector.py
Original file line number Diff line number Diff line change
@@ -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__)

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -110,15 +114,16 @@ def __init__(self, username, password, meter_id, show_generation=False, show_12_

def get_raw_data(self) -> TauronAmiplusRawData:
data = TauronAmiplusRawData()
self.login()
# amount_value, amount_status = self.get_moj_tauron()
# data.amount_value = amount_value
# data.amount_status = amount_status
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:
data.tariff = data.consumption.json_yearly["data"]["tariff"]
self._cache.delete_older_than(min(consumption_max_cache, generation_max_cache))
return data

Expand Down Expand Up @@ -149,68 +154,77 @@ 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
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):
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".*\"}'>(.*)</option>", 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):
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 login")
else:
return None
tariff = json_data["data"]["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)
Expand All @@ -220,6 +234,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)
Expand All @@ -240,6 +255,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:
Expand Down Expand Up @@ -293,7 +309,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
Expand All @@ -316,6 +333,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)
Expand Down Expand Up @@ -346,15 +364,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
Expand All @@ -363,6 +386,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("<span class=\"amount-value\">", "")
.replace("zł", "")
.replace("</span>", "")
.replace(",", ".").strip())
amount_status = (find_2_1[0].strip()
.replace("<span class=\"amount-status\">", "")
.replace("</span>", "").strip())
return amount_value, amount_status

find_1_2 = re.findall(r".*class=\"amount\".*\s*.*\s*</div>", 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("<div class=\"amount\">", "")
.replace("zł", "")
.replace("</div>", "")
.replace(",", ".").strip())
amount_status = (find_2_2[0].strip()
.replace("<div class=\"date\">", "")
.replace("</div>", "").strip())
return amount_value, amount_status

return None, "unknown"

@staticmethod
def format_date(date):
return date.strftime(CONST_DATE_FORMAT)
Expand All @@ -373,16 +431,15 @@ 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):
connector = TauronAmiplusConnector(username, password, meter_id)
connector.login()
config = connector.calculate_configuration()
if config is not None:
return config
raise Exception("Failed to login")
tariff = connector.login()
if tariff is not None:
return tariff
raise Exception("Failed to calculate configuration")

@staticmethod
def add_all_data(data: dict, date):
Expand Down
Loading

0 comments on commit 3c58741

Please sign in to comment.