|
| 1 | +import datetime |
| 2 | +import json |
| 3 | +import logging |
| 4 | +import pathlib |
| 5 | +from typing import Union |
| 6 | + |
| 7 | +import requests |
| 8 | + |
| 9 | +import settings |
| 10 | + |
| 11 | +BASE_URL = "https://smartmeter.netz-noe.at/orchestration" |
| 12 | + |
| 13 | +_logger = logging.getLogger(__name__) |
| 14 | +_settings = settings.Settings() |
| 15 | + |
| 16 | + |
| 17 | +class SmartMeter: |
| 18 | + def __init__(self, username: str, password: str) -> None: |
| 19 | + self.session = requests.Session() |
| 20 | + self.session.headers.update({"User-Agent": _settings.user_agent}) |
| 21 | + |
| 22 | + self._login(username, password) |
| 23 | + |
| 24 | + def _login(self, username: str, password: str) -> None: |
| 25 | + """Log into the smartmeter portal from Netz NÖ.""" |
| 26 | + url = f"{BASE_URL}/Authentication/Login" |
| 27 | + response = self.session.post(url, json={"user": username, "pwd": password}) |
| 28 | + response.raise_for_status() |
| 29 | + |
| 30 | + def _get_account_ids(self) -> list[str]: |
| 31 | + """ |
| 32 | + Retrieve account ids linked to the login. |
| 33 | +
|
| 34 | + :return: list of account ids |
| 35 | + """ |
| 36 | + url = f"{BASE_URL}/User/GetAccountIdByBussinespartnerId" |
| 37 | + |
| 38 | + response = self.session.get(url) |
| 39 | + response.raise_for_status() |
| 40 | + _logger.debug("Response for '%s' was: %s", url, json.dumps(response.json())) |
| 41 | + |
| 42 | + return [account["accountId"] for account in response.json()] |
| 43 | + |
| 44 | + def _get_meters(self, account_id: str) -> list[str]: |
| 45 | + """ |
| 46 | + Retrieve metering points for the given account id. |
| 47 | +
|
| 48 | + :param account_id: account id |
| 49 | + :return: list of metering points |
| 50 | + """ |
| 51 | + url = f"{BASE_URL}/User/GetMeteringPointByAccountId" |
| 52 | + |
| 53 | + response = self.session.get(url, params={"accountId": account_id}) |
| 54 | + response.raise_for_status() |
| 55 | + _logger.debug("Response for '%s' was: %s", url, json.dumps(response.json())) |
| 56 | + |
| 57 | + return [entry["meteringPointId"] for entry in response.json()] |
| 58 | + |
| 59 | + def get_account_meters(self) -> dict[str, list[str]]: |
| 60 | + """ |
| 61 | + Retrieves all metering points and account-ids linked to the login. |
| 62 | +
|
| 63 | + :return: Dictionary with |
| 64 | + key: account-id |
| 65 | + value: list of metering points for the account |
| 66 | + """ |
| 67 | + return { |
| 68 | + account_id: self._get_meters(account_id) |
| 69 | + for account_id in self._get_account_ids() |
| 70 | + } |
| 71 | + |
| 72 | + def _get_mean_profile_for_day( |
| 73 | + self, metering_point: str, day: datetime.date |
| 74 | + ) -> dict[str, list[Union[None, str, float]]]: |
| 75 | + """Retrieve mean profile for the given day.""" |
| 76 | + url = f"{BASE_URL}/ConsumptionRecord/MeanProfileDay" |
| 77 | + |
| 78 | + response = self.session.get( |
| 79 | + url, params={"meterId": metering_point, "day": day.isoformat()} |
| 80 | + ) |
| 81 | + response.raise_for_status() |
| 82 | + _logger.debug("Response for '%s' was: %s", url, json.dumps(response.json())) |
| 83 | + |
| 84 | + return response.json() |
| 85 | + |
| 86 | + def get_consumption_records_for_day( |
| 87 | + self, |
| 88 | + metering_point: str, |
| 89 | + day: datetime.date, |
| 90 | + *, |
| 91 | + include_mean_profile: bool = True, |
| 92 | + ) -> dict[str, list[Union[None, str, float]]]: |
| 93 | + """ |
| 94 | + Retrieve the consumption records for the given day and the given metering point. |
| 95 | +
|
| 96 | + :param metering_point: metering point |
| 97 | + :param day: day |
| 98 | + :param include_mean_profile: indicates if mean profile of the day should be included |
| 99 | + """ |
| 100 | + url = f"{BASE_URL}/ConsumptionRecord/Day" |
| 101 | + |
| 102 | + response = self.session.get( |
| 103 | + url, params={"meterId": metering_point, "day": day.isoformat()} |
| 104 | + ) |
| 105 | + response.raise_for_status() |
| 106 | + _logger.debug("Response for '%s' was: %s", url, json.dumps(response.json())) |
| 107 | + |
| 108 | + if not include_mean_profile: |
| 109 | + return response.json() |
| 110 | + |
| 111 | + mean_profile = self._get_mean_profile_for_day(metering_point, day) |
| 112 | + return response.json() | {"meanProfile": mean_profile} |
| 113 | + |
| 114 | + |
| 115 | +def download_consumptions_for_meter( |
| 116 | + smartmeter: SmartMeter, |
| 117 | + storage_path: pathlib.Path, |
| 118 | + energy_meter: str, |
| 119 | + start_date: datetime.date, |
| 120 | +): |
| 121 | + """ |
| 122 | + Download consumption records starting from start-date for given engery-meter. |
| 123 | + """ |
| 124 | + number_since_measure_start = (datetime.date.today() - start_date).days |
| 125 | + for day in [ |
| 126 | + start_date + datetime.timedelta(days=i) |
| 127 | + for i in range(number_since_measure_start) |
| 128 | + ]: |
| 129 | + json_file = storage_path / f"{day.isoformat()}.json" |
| 130 | + if json_file.exists(): |
| 131 | + _logger.debug( |
| 132 | + "Consumption records for '%s' and '%s' already exists.", |
| 133 | + energy_meter, |
| 134 | + day.isoformat(), |
| 135 | + ) |
| 136 | + continue |
| 137 | + |
| 138 | + json_file.write_text( |
| 139 | + json.dumps( |
| 140 | + smartmeter.get_consumption_records_for_day(energy_meter, day), |
| 141 | + indent=4, |
| 142 | + ) |
| 143 | + ) |
| 144 | + |
| 145 | + |
| 146 | +def main(): |
| 147 | + smartmeter = SmartMeter(_settings.username, _settings.password) |
| 148 | + acount_meters = smartmeter.get_account_meters() |
| 149 | + for account_id, energy_meters in acount_meters.items(): |
| 150 | + account_dir = _settings.storage_path / account_id |
| 151 | + if not account_dir.exists(): |
| 152 | + account_dir.mkdir() |
| 153 | + |
| 154 | + for energy_meter in energy_meters: |
| 155 | + energy_meter_dir = account_dir / energy_meter |
| 156 | + if not energy_meter_dir.exists(): |
| 157 | + energy_meter_dir.mkdir() |
| 158 | + |
| 159 | + download_consumptions_for_meter( |
| 160 | + smartmeter, energy_meter_dir, energy_meter, _settings.measure_start_date |
| 161 | + ) |
| 162 | + |
| 163 | + |
| 164 | +if __name__ == "__main__": |
| 165 | + logging.basicConfig(level=logging.DEBUG) |
| 166 | + |
| 167 | + main() |
0 commit comments