pax_global_header00006660000000000000000000000064146773022340014522gustar00rootroot0000000000000052 comment=c53baccc9e8045326ff55964bc9280857bcc0cba sunweg-3.1.0/000077500000000000000000000000001467730223400130335ustar00rootroot00000000000000sunweg-3.1.0/.github/000077500000000000000000000000001467730223400143735ustar00rootroot00000000000000sunweg-3.1.0/.github/workflows/000077500000000000000000000000001467730223400164305ustar00rootroot00000000000000sunweg-3.1.0/.github/workflows/python-build.yml000066400000000000000000000025161467730223400215750ustar00rootroot00000000000000name: Python build on: push: branches: ["main"] pull_request: branches: ["main"] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.11", "3.10"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest pytest-cov genbadge[all] if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --output-file ./reports/flake8/flake8stats.txt - name: Test with pytest run: | pytest --cov=sunweg --cov-report html --cov-report xml --junitxml=reports/junit/junit.xml mv htmlcov reports/coverage - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 sunweg-3.1.0/.github/workflows/python-publish.yml000066400000000000000000000054621467730223400221470ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest pytest-cov genbadge[all] if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --output-file ./reports/flake8/flake8stats.txt - name: Test with pytest run: | pytest --cov=sunweg --cov-report html --cov-report xml --junitxml=reports/junit/junit.xml mv htmlcov reports/coverage - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 - name: Generate badges if: github.event_name != 'pull_request' run: | genbadge tests -o reports/tests.svg genbadge coverage -i coverage.xml -o reports/coverage.svg genbadge flake8 -i reports/flake8/flake8stats.txt -o reports/flake8.svg - name: Publish badges report to badges branch uses: JamesIves/github-pages-deploy-action@v4 if: github.event_name != 'pull_request' with: branch: badges folder: reports token: ${{ secrets.SUNWEG_GITHUB_PAT }} deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: "3.10" - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} sunweg-3.1.0/.gitignore000066400000000000000000000034201467730223400150220ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ reports/ sunweg-3.1.0/.vscode/000077500000000000000000000000001467730223400143745ustar00rootroot00000000000000sunweg-3.1.0/.vscode/extensions.json000066400000000000000000000002271467730223400174670ustar00rootroot00000000000000{ "recommendations": [ "esbenp.prettier-vscode", "ms-python.python", "github.vscode-pull-request-github", "charliermarsh.ruff" ] } sunweg-3.1.0/.vscode/launch.json000066400000000000000000000007311467730223400165420ustar00rootroot00000000000000{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Python Debugger: Current File", "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal" } ] }sunweg-3.1.0/.vscode/settings.json000066400000000000000000000024241467730223400171310ustar00rootroot00000000000000{ "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, "python.formatting.provider": "black", "python.testing.pytestArgs": [ "tests" ], "python.testing.pytestEnabled": true, "python.linting.mypyEnabled": true, "cSpell.ignoreWords": [ "Acumulada", "Acumulado", "Atualizacao", "Corrente", "Gerada", "Hoje", "Inversor", "Potencia", "SUNWEG", "Tensao", "alarme", "autenticacao", "carbono", "descricao", "economia", "energia", "energiaacumuladanumber", "fatorpotencia", "franqueado", "frequencia", "getpaineloperacao", "idusina", "integrador", "inversores", "mppt", "mppts", "nomemppt", "planos", "potenciaativa", "procurar", "reduz", "rtype", "senha", "situacao", "stringmppt", "temperatura", "tensaoca", "usinas", "usuario", "rokam" ], "python.linting.mypyArgs": [ "--follow-imports=silent", "--ignore-missing-imports", "--show-column-numbers", "--no-pretty", "--install-types" ], "python.testing.unittestEnabled": false, "python.linting.flake8Enabled": true, "python.linting.flake8Args": [ "--max-complexity=10", "--max-line-length=127" ], } sunweg-3.1.0/.vscode/tasks.json000066400000000000000000000010431467730223400164120ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "label": "Code Coverage", "detail": "Generate code coverage report.", "type": "shell", "command": "pytest ./tests/ --cov=sunweg --cov-report term-missing --durations-min=1 --durations=0", "group": { "kind": "test", "isDefault": true }, "presentation": { "reveal": "always", "panel": "new" }, "problemMatcher": [] } ] }sunweg-3.1.0/LICENSE000066400000000000000000000020731467730223400140420ustar00rootroot00000000000000MIT License Copyright (c) 2023 Lucas Mindêllo de Andrade Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sunweg-3.1.0/README.md000066400000000000000000000027731467730223400143230ustar00rootroot00000000000000# SunWeg [![Python build](https://github.com/rokam/sunweg/actions/workflows/python-build.yml/badge.svg)](https://github.com/rokam/sunweg/actions/workflows/python-build.yml) ![Python tests](https://raw.githubusercontent.com/rokam/sunweg/badges/tests.svg) ![Python coverage](https://raw.githubusercontent.com/rokam/sunweg/badges/coverage.svg) ![Python fake8](https://raw.githubusercontent.com/rokam/sunweg/badges/flake8.svg) Python lib for WEG solar energy platform, [SunWEG.net](https://sunweg.net/) ## Usage ### Retrieve token You need to use devtools from your browser to retrieve the token. In most of them, you can open devtools by pressing F12. Inside the network tab, you need to check the preserve logs and then do a login. You can find the token in the request header of various XHR requests, for example: ![Devtools with a request with a token](img/image.png) It will be available in the X-Auth-Token-Update header. ### Code sample ``` python from sunweg.api import APIHelper api = APIHelper(token='your token here') plants = api.listPlants() for plant in plants: print(plant) for inverter in plant.inverters: print(inverter) for phase in inverter.phases: print(phase) for mppt in inverter.mppts: print(mppt) for string in mppt.strings: print(string) ``` ## Documentation Check the [DOCs](https://github.com/rokam/sunweg/blob/main/docs/index.md) for API documentation. ## Contribute Feel free to send issues and pull requests. sunweg-3.1.0/docs/000077500000000000000000000000001467730223400137635ustar00rootroot00000000000000sunweg-3.1.0/docs/index.md000066400000000000000000000611741467730223400154250ustar00rootroot00000000000000# Table of Contents * [sunweg](#sunweg) * [sunweg.device](#sunweg.device) * [Phase](#sunweg.device.Phase) * [\_\_init\_\_](#sunweg.device.Phase.__init__) * [name](#sunweg.device.Phase.name) * [voltage](#sunweg.device.Phase.voltage) * [amperage](#sunweg.device.Phase.amperage) * [status\_voltage](#sunweg.device.Phase.status_voltage) * [status\_amperage](#sunweg.device.Phase.status_amperage) * [\_\_str\_\_](#sunweg.device.Phase.__str__) * [String](#sunweg.device.String) * [\_\_init\_\_](#sunweg.device.String.__init__) * [name](#sunweg.device.String.name) * [voltage](#sunweg.device.String.voltage) * [amperage](#sunweg.device.String.amperage) * [status](#sunweg.device.String.status) * [\_\_str\_\_](#sunweg.device.String.__str__) * [MPPT](#sunweg.device.MPPT) * [\_\_init\_\_](#sunweg.device.MPPT.__init__) * [name](#sunweg.device.MPPT.name) * [strings](#sunweg.device.MPPT.strings) * [\_\_str\_\_](#sunweg.device.MPPT.__str__) * [Inverter](#sunweg.device.Inverter) * [\_\_init\_\_](#sunweg.device.Inverter.__init__) * [id](#sunweg.device.Inverter.id) * [name](#sunweg.device.Inverter.name) * [sn](#sunweg.device.Inverter.sn) * [status](#sunweg.device.Inverter.status) * [temperature](#sunweg.device.Inverter.temperature) * [today\_energy](#sunweg.device.Inverter.today_energy) * [today\_energy](#sunweg.device.Inverter.today_energy) * [today\_energy\_metric](#sunweg.device.Inverter.today_energy_metric) * [today\_energy\_metric](#sunweg.device.Inverter.today_energy_metric) * [total\_energy](#sunweg.device.Inverter.total_energy) * [total\_energy](#sunweg.device.Inverter.total_energy) * [total\_energy\_metric](#sunweg.device.Inverter.total_energy_metric) * [total\_energy\_metric](#sunweg.device.Inverter.total_energy_metric) * [power\_factor](#sunweg.device.Inverter.power_factor) * [power\_factor](#sunweg.device.Inverter.power_factor) * [frequency](#sunweg.device.Inverter.frequency) * [frequency](#sunweg.device.Inverter.frequency) * [power](#sunweg.device.Inverter.power) * [power](#sunweg.device.Inverter.power) * [power\_metric](#sunweg.device.Inverter.power_metric) * [power\_metric](#sunweg.device.Inverter.power_metric) * [is\_complete](#sunweg.device.Inverter.is_complete) * [phases](#sunweg.device.Inverter.phases) * [mppts](#sunweg.device.Inverter.mppts) * [\_\_str\_\_](#sunweg.device.Inverter.__str__) * [sunweg.const](#sunweg.const) * [SUNWEG\_URL](#sunweg.const.SUNWEG_URL) * [SUNWEG\_LOGIN\_PATH](#sunweg.const.SUNWEG_LOGIN_PATH) * [SUNWEG\_PLANT\_LIST\_PATH](#sunweg.const.SUNWEG_PLANT_LIST_PATH) * [SUNWEG\_PLANT\_DETAIL\_PATH](#sunweg.const.SUNWEG_PLANT_DETAIL_PATH) * [SUNWEG\_INVERTER\_DETAIL\_PATH](#sunweg.const.SUNWEG_INVERTER_DETAIL_PATH) * [SUNWEG\_MONTH\_STATS\_PATH](#sunweg.const.SUNWEG_MONTH_STATS_PATH) * [sunweg.api](#sunweg.api) * [SunWegApiError](#sunweg.api.SunWegApiError) * [LoginError](#sunweg.api.LoginError) * [convert\_situation\_status](#sunweg.api.convert_situation_status) * [separate\_value\_metric](#sunweg.api.separate_value_metric) * [APIHelper](#sunweg.api.APIHelper) * [\_\_init\_\_](#sunweg.api.APIHelper.__init__) * [authenticate](#sunweg.api.APIHelper.authenticate) * [listPlants](#sunweg.api.APIHelper.listPlants) * [plant](#sunweg.api.APIHelper.plant) * [inverter](#sunweg.api.APIHelper.inverter) * [complete\_inverter](#sunweg.api.APIHelper.complete_inverter) * [month\_stats\_production](#sunweg.api.APIHelper.month_stats_production) * [month\_stats\_production\_by\_id](#sunweg.api.APIHelper.month_stats_production_by_id) * [sunweg.plant](#sunweg.plant) * [Plant](#sunweg.plant.Plant) * [\_\_init\_\_](#sunweg.plant.Plant.__init__) * [id](#sunweg.plant.Plant.id) * [name](#sunweg.plant.Plant.name) * [total\_power](#sunweg.plant.Plant.total_power) * [kwh\_per\_kwp](#sunweg.plant.Plant.kwh_per_kwp) * [performance\_rate](#sunweg.plant.Plant.performance_rate) * [saving](#sunweg.plant.Plant.saving) * [today\_energy](#sunweg.plant.Plant.today_energy) * [today\_energy\_metric](#sunweg.plant.Plant.today_energy_metric) * [total\_energy](#sunweg.plant.Plant.total_energy) * [total\_carbon\_saving](#sunweg.plant.Plant.total_carbon_saving) * [last\_update](#sunweg.plant.Plant.last_update) * [inverters](#sunweg.plant.Plant.inverters) * [\_\_str\_\_](#sunweg.plant.Plant.__str__) * [sunweg.util](#sunweg.util) * [Status](#sunweg.util.Status) * [ProductionStats](#sunweg.util.ProductionStats) * [\_\_init\_\_](#sunweg.util.ProductionStats.__init__) * [date](#sunweg.util.ProductionStats.date) * [production](#sunweg.util.ProductionStats.production) * [prognostic](#sunweg.util.ProductionStats.prognostic) * [\_\_str\_\_](#sunweg.util.ProductionStats.__str__) # sunweg Sunweg API library. # sunweg.device Sunweg API devices. ## Phase Objects ```python class Phase() ``` Phase details. #### \_\_init\_\_ ```python def __init__(name: str, voltage: float, amperage: float, status_voltage: Status, status_amperage: Status) -> None ``` Initialize Phase. **Arguments**: - `name` (`str`): phase name - `voltage` (`float`): phase AC voltage in V - `amperage` (`float`): phase AC amperage in A - `status_voltage` (`Status`): phase AC voltage status - `status_amperage` (`Status`): phase AC amperage status #### name ```python @property def name() -> str ``` Get phase name. **Returns**: `str`: phase name #### voltage ```python @property def voltage() -> float ``` Get phase AC voltage in V. **Returns**: `float`: phase AC voltage in V #### amperage ```python @property def amperage() -> float ``` Get phase AC amperage in A. **Returns**: `float`: phase AC amperage in A #### status\_voltage ```python @property def status_voltage() -> Status ``` Get phase AC voltage status. **Returns**: `Status`: phase AC voltage status #### status\_amperage ```python @property def status_amperage() -> Status ``` Get phase AC amperage status. **Returns**: `Status`: phase AC amperage status #### \_\_str\_\_ ```python def __str__() -> str ``` Cast Phase to str. ## String Objects ```python class String() ``` String details. #### \_\_init\_\_ ```python def __init__(name: str, voltage: float, amperage: float, status: Status) -> None ``` Initialize String. **Arguments**: - `name` (`str`): string name - `voltage` (`float`): string DC voltage in V - `amperage` (`float`): string DC amperage in A - `status` (`Status`): string status #### name ```python @property def name() -> str ``` Get string name. **Returns**: `str`: string name #### voltage ```python @property def voltage() -> float ``` Get string DC voltage in V. **Returns**: `float`: string DC voltage in V #### amperage ```python @property def amperage() -> float ``` Get string DC amperage in A. **Returns**: `float`: string DC amperage in A #### status ```python @property def status() -> Status ``` Get string status. **Returns**: `Status`: string status #### \_\_str\_\_ ```python def __str__() -> str ``` Cast String to str. ## MPPT Objects ```python class MPPT() ``` MPPT details. #### \_\_init\_\_ ```python def __init__(name: str) -> None ``` Initialize MPPT. **Arguments**: - `name` (`srt`): MPPT name #### name ```python @property def name() -> str ``` Get MPPT name. **Returns**: `str`: MPPT name #### strings ```python @property def strings() -> list[String] ``` Get list of MPPT's String. **Returns**: `list[String]`: list of Strings #### \_\_str\_\_ ```python def __str__() -> str ``` Cast MPPT to str. ## Inverter Objects ```python class Inverter() ``` Inverter device. #### \_\_init\_\_ ```python def __init__(id: int, name: str, sn: str, status: Status, temperature: int, total_energy: float = 0, total_energy_metric: str = "", today_energy: float = 0, today_energy_metric: str = "", power_factor: float = 0, frequency: float = 0, power: float = 0, power_metric: str = "") -> None ``` Initialize Inverter. **Arguments**: - `id` (`int`): inverter id - `name` (`str`): inverter name - `sn` (`str`): inverter serial number - `status` (`Status`): inverter status - `temperature` (`int`): inverter temperature - `total_energy` (`float`): total generated energy - `total_energy_metric` (`str`): total generated energy metric - `today_energy` (`float`): total generated energy today - `today_energy_metric` (`str`): total generated energy today metric - `power_factor` (`float`): inverter power factor - `frequency` (`float`): inverter output frequency in Hz - `power` (`str`): inverter output power - `power` (`str`): inverter output power metric #### id ```python @property def id() -> int ``` Get inverter id. **Returns**: `int`: inverter id #### name ```python @property def name() -> str ``` Get inverter name. **Returns**: `str`: inverter name #### sn ```python @property def sn() -> str ``` Get inverter serial number. **Returns**: `str`: inverter serial number #### status ```python @property def status() -> Status ``` Get inverter status. **Returns**: `Status`: inverter status #### temperature ```python @property def temperature() -> int ``` Get inverter temperature. **Returns**: `int`: inverter temperature #### today\_energy ```python @property def today_energy() -> float ``` Get inverter today generated energy. **Returns**: `float`: inverter today generated energy #### today\_energy ```python @today_energy.setter def today_energy(value: float) -> None ``` Set inverter today generated energy. **Arguments**: - `value` (`float`): inverter today generated energy #### today\_energy\_metric ```python @property def today_energy_metric() -> str ``` Get inverter today generated energy metric. **Returns**: `str`: inverter today generated energy metric #### today\_energy\_metric ```python @today_energy_metric.setter def today_energy_metric(value: str) -> None ``` Set inverter today generated energy metric. **Arguments**: - `value` (`str`): inverter today generated energy metric #### total\_energy ```python @property def total_energy() -> float ``` Get inverter total generated energy. **Returns**: `float`: inverter total generated energy #### total\_energy ```python @total_energy.setter def total_energy(value: float) -> None ``` Set inverter total generated energy. **Arguments**: - `value` (`float`): inverter total generated energy #### total\_energy\_metric ```python @property def total_energy_metric() -> str ``` Get inverter total generated energy metric. **Returns**: `str`: inverter total generated energy metric #### total\_energy\_metric ```python @total_energy_metric.setter def total_energy_metric(value: str) -> None ``` Set inverter total generated energy metric. **Arguments**: - `value` (`str`): inverter total generated energy metric #### power\_factor ```python @property def power_factor() -> float ``` Get inverter power factor. **Returns**: `float`: inverter power factor #### power\_factor ```python @power_factor.setter def power_factor(value: float) -> None ``` Set inverter power factor. **Arguments**: - `value` (`float`): inverter power factor #### frequency ```python @property def frequency() -> float ``` Get inverter frequency in Hz. **Returns**: `float`: inverter frequency in HZ #### frequency ```python @frequency.setter def frequency(value: float) -> None ``` Set inverter frequency in Hz. **Arguments**: - `value` (`float`): inverter frequency in Hz #### power ```python @property def power() -> float ``` Get inverter output power. **Returns**: `float`: inverter output power #### power ```python @power.setter def power(value: float) -> None ``` Set inverter output power. **Arguments**: - `value` (`float`): inverter output power #### power\_metric ```python @property def power_metric() -> str ``` Get inverter output power metric. **Returns**: `str`: inverter output power metric #### power\_metric ```python @power_metric.setter def power_metric(value: str) -> None ``` Set inverter output power metric. **Arguments**: - `value` (`float`): inverter output power metric #### is\_complete ```python @property def is_complete() -> bool ``` Is inverter data complete. **Returns**: `bool`: True when inverter data is complete #### phases ```python @property def phases() -> list[Phase] ``` Get list of inverter's phases. **Returns**: `list[Phase]`: list of phases #### mppts ```python @property def mppts() -> list[MPPT] ``` Get list of inverter's MPPTs. **Returns**: `list[MPPT]`: list of MPPTs #### \_\_str\_\_ ```python def __str__() -> str ``` Cast Inverter to str. # sunweg.const Sunweg API constants. #### SUNWEG\_URL SunWEG API URL #### SUNWEG\_LOGIN\_PATH SunWEG API login path #### SUNWEG\_PLANT\_LIST\_PATH SunWEG API list plants path #### SUNWEG\_PLANT\_DETAIL\_PATH SunWEG API plant details path #### SUNWEG\_INVERTER\_DETAIL\_PATH SunWEG API inverter details path #### SUNWEG\_MONTH\_STATS\_PATH SunWEG API month history path # sunweg.api API Helper. ## SunWegApiError Objects ```python class SunWegApiError(RuntimeError) ``` API Error. ## LoginError Objects ```python class LoginError(SunWegApiError) ``` Login Error. #### convert\_situation\_status ```python def convert_situation_status(situation: int) -> Status ``` Convert situation to status. **Arguments**: - `situation` (`int`): situation **Returns**: `Status`: equivalent status #### separate\_value\_metric ```python def separate_value_metric(value_with_metric: str | None, default_metric: str = "") -> tuple[float, str] ``` Separate the value from the metric. **Arguments**: - `value_with_metric` (`str | None`): value with metric separated by space - `default_metric` (`str`): metric that should be returned if `value_with_metric` is None **Returns**: `tuple[float, str]`: tuple with value and metric ## APIHelper Objects ```python class APIHelper() ``` Class to call sunweg.net api. #### \_\_init\_\_ ```python def __init__(username: str, password: str) -> None ``` Initialize APIHelper for SunWEG platform. **Arguments**: - `username` (`str`): username for authentication - `password` (`str`): password for authentication #### authenticate ```python def authenticate() -> bool ``` Authenticate with provided username and password. **Returns**: `bool`: True on authentication success #### listPlants ```python def listPlants(retry=True) -> list[Plant] ``` Retrieve the list of plants with incomplete inverter information. You may want to call `complete_inverter()` to complete the Inverter information. **Arguments**: - `retry` (`bool`): reauthenticate if token expired and retry **Returns**: `list[Plant]`: list of Plant #### plant ```python def plant(id: int, retry=True) -> Plant | None ``` Retrieve plant detail by plant id. **Arguments**: - `id` (`int`): plant id - `retry` (`bool`): reauthenticate if token expired and retry **Returns**: `Plant | None`: Plant or None if `id` not found. #### inverter ```python def inverter(id: int, retry=True) -> Inverter | None ``` Retrieve inverter detail by inverter id. **Arguments**: - `id` (`int`): inverter id - `retry` (`bool`): reauthenticate if token expired and retry **Returns**: `Inverter | None`: Inverter or None if `id` not found. #### complete\_inverter ```python def complete_inverter(inverter: Inverter, retry=True) -> None ``` Complete inverter data. **Arguments**: - `inverter` (`Inverter`): inverter object to be completed with information - `retry` (`bool`): reauthenticate if token expired and retry #### month\_stats\_production ```python def month_stats_production(year: int, month: int, plant: Plant, inverter: Inverter | None = None, retry: bool = True) -> list[ProductionStats] ``` Retrieve month energy production statistics. **Arguments**: - `year` (`int`): statistics year - `month` (`int`): statistics month - `plant` (`Plant`): statistics plant - `inverter` (`Inverter | None`): statistics inverter, None for every inverter - `retry` (`bool`): reauthenticate if token expired and retry **Returns**: `list[ProductionStats]`: list of daily energy production statistics #### month\_stats\_production\_by\_id ```python def month_stats_production_by_id(year: int, month: int, plant_id: int, inverter_id: int | None = None, retry: bool = True) -> list[ProductionStats] ``` Retrieve month energy production statistics. **Arguments**: - `year` (`int`): statistics year - `month` (`int`): statistics month - `plant_id` (`int`): id of statistics plant - `inverter_id` (`int | None`): id of statistics inverter, None for every inverter - `retry` (`bool`): reauthenticate if token expired and retry **Returns**: `list[ProductionStats]`: list of daily energy production statistics # sunweg.plant Sunweg API plant. ## Plant Objects ```python class Plant() ``` Plant details. #### \_\_init\_\_ ```python def __init__(id: int, name: str, total_power: float, kwh_per_kwp: float, performance_rate: float, saving: float, today_energy: float, today_energy_metric: str, total_energy: float, total_carbon_saving: float, last_update: datetime | None) -> None ``` Initialize Plant. **Arguments**: - `id` (`int`): plant id - `name` (`str`): plant name - `total_power` (`float`): plant total power - `kwh_per_kwp` (`float`): plant kWh/kWp - `performance_rate` (`float`): plant performance rate - `saving` (`float`): total saving in R$ - `today_energy` (`float`): today generated energy - `today_energy_metric` (`str`): today generated energy metric - `total_energy` (`float`): total generated energy in kWh - `total_carbon_saving` (`float`): total of CO2 saved - `last_update` (`datetime | None`): when the data was updated #### id ```python @property def id() -> int ``` Get plant id. **Returns**: `int`: plant id #### name ```python @property def name() -> str ``` Get plant name. **Returns**: `str`: plant name #### total\_power ```python @property def total_power() -> float ``` Get plant total power. **Returns**: `float`: plant total power #### kwh\_per\_kwp ```python @property def kwh_per_kwp() -> float ``` Get plant kWh/kWp. **Returns**: `float`: plant kWh/kWp #### performance\_rate ```python @property def performance_rate() -> float ``` Get plant performance rate. **Returns**: `float`: plant performance rate #### saving ```python @property def saving() -> float ``` Get plant saving in R$. **Returns**: `float`: plant saving in R$ #### today\_energy ```python @property def today_energy() -> float ``` Get plant today generated energy. **Returns**: `float`: plant today generated energy #### today\_energy\_metric ```python @property def today_energy_metric() -> str ``` Get plant today generated energy metric. **Returns**: `str`: plant today generated energy metric #### total\_energy ```python @property def total_energy() -> float ``` Get plant total generated energy in kWh. **Returns**: `float`: plant total generated energy in kWh #### total\_carbon\_saving ```python @property def total_carbon_saving() -> float ``` Get plant total of CO2 saved. **Returns**: `float`: plant total of CO2 saved #### last\_update ```python @property def last_update() -> datetime | None ``` Get when the plant data was updated. **Returns**: `datetime | None`: when the plant data was updated #### inverters ```python @property def inverters() -> list[Inverter] ``` Get list of plant's inverters. **Returns**: `list[Inverter]`: list of inverters #### \_\_str\_\_ ```python def __str__() -> str ``` Cast Plant to str. # sunweg.util Sunweg API util. ## Status Objects ```python class Status(Enum) ``` Status enum. ## ProductionStats Objects ```python class ProductionStats() ``` Energy production statistics #### \_\_init\_\_ ```python def __init__(date: date, production: float, prognostic: float) -> None ``` Initialize energy production statistics. **Arguments**: - `date` (`date`): statistics date - `production` (`float`): statistics production in kWh - `prognostic`: statistics expected production in kWh #### date ```python @property def date() -> date ``` Get date. #### production ```python @property def production() -> float ``` Get energy production in kWh. #### prognostic ```python @property def prognostic() -> float ``` Get expected energy production in kWh. #### \_\_str\_\_ ```python def __str__() -> str ``` Cast Phase to str. sunweg-3.1.0/img/000077500000000000000000000000001467730223400136075ustar00rootroot00000000000000sunweg-3.1.0/img/image.png000066400000000000000000004244121467730223400154060ustar00rootroot00000000000000PNG  IHDR#b<sBIT|dtEXtSoftwaregnome-screenshot>&tEXtCreation Timequa 02 out 2024 14:25:34Kz IDATx}\TUAġ&ACK2)ݠ-E\2*ž l1-MV2`,Vt[MCEJ!$`?ay~|{9{>{|> 5"`ҖC8e:Iyԝe]FVp#Gߔ)~ٳ6uпs.@ 3Rnt9~tMgkX.lhs`0欬-bz ^^ * dm1V CVVX/eco|#d!""3C}cbbHH@ Ucp-o we@6VԝcҖ-w: 0ّGst[j""FEVf9Bɑ# CCg@|B<[uع;@E*/? YYVKV>50e*çޡtj9e=?# #M-' #%h }^n"""Yຟ@ F;Uc].嗅dlF(7~CxW˼M. 0T&?ؘXEwPŨ":Ěn"""<.rpd :bw`9=Z&6&Zo4Avw{?O[" )@ 3j$T=,ob7L eQaLyfI~9ŷE{H-={H#I~!ڟ‡I Hh;]u[17e!@wm4jD/fsVQXZZZ:4tjuǼ'`ɑ#^3(@ ]%"iE[|!lm6D~(8{αΎ]^?24tFʲ`M?>9ENT WB%/?9GJo s5Oö +_ ْO daBaW<fE>`~ܹ]gL0 IZL7ri3ÆQQ3zoL^-tM?}teK4ziqU)9R™ "DQ%GJ\[i,3--X*e1@ `"-- 9~ ~pa.Z//= ; @'aeY(uJ&'X9gUJQpHKsز+n{X:AwCC5OSI1^\i8AF aM"9kЎ qf6@l*w< WIǍxg#[[at`hmX/MX3&66ɉ#%"4\ @ tPJ5FWuƨxR6o^sC獑j+E@KHW#+ X؅1!`h۳=JL'xVbXwORU]ȔLO\ETDtQb́q3Y0 {E:L$8:]6髲8DZ {9t2zYeHLyx>}\0RJ~ aτ૶('ɛ&5f]$W{[lp%b4\*w!ECWl#,nӸHG{>DLrJ[Ԋs}qrf٫AYޮsPf_4uIHJRFBB,}n ɧ pǕ8/=J/_Il{tuNmNC`XcP69+[k~se$ˎ_]ʳL;iM mfCˋ@ @p% ы%441r6ViITvV6Ϛ!.yeXZgi"`ް[6KDDD@VJ~L~W8NM ]N5 ;ݐbɪ2 >L!%eN~,|qK^'s&s]vc6I #3Jz~3'^Nq#]싏]%|plg:F<Db3F\X%7Y Kt֑" ""\{BYWB̲ ]iAY*4XJdPHulL"'.\ά]$4ڋ*Y]42H_S-3ǜF\MhS@52;KCdCx#[¦StΒ+%*d\(糷3)1 _ (Z B5dsןU9-ιL%)-S3ѿ!6Wubۻd&)e*VIĿxْTdLR=˳+hN$tOpA ,_TD㸹 Uג5EF2DȚv3+Uc&--LM@ A%f#㕘5ļAFg"4z60FgC+ZaMkJiވ@m^?9< )g!g3ɥM0;]nlO(xQ&ɏ=vc3:g_&-)1ySS-9y++Y~!3.GAÒ4\O=vdSp#Lp)+y5aIvtL\!JeTu~8[AY0x鹨/On#+Tv~֮]C^r3nAKCˋÕcbbqm|'2kPIa@ ]!)1 Ȧ qqwCv3)nJ!`Xo6 K6geOD4S^^^̓6j/.x:6.o 5 -C8e >H pl|IL'&.]:OUዺ$v^F+f[R {[_c M ϟGSP\m#i6%T@]Kܘ 3g.!)>d2#b2l2U~I&A`j}t\1$&o97H`+AVtTSi&'GK2K\~beoKa>=EΝhKj5Cg"!!ZZM@|B<ΰ^h g!R @ t&',JP<+~jސ̻f[ odO]s1 demF%&&HdemRyn{-^KA}gv6 $$ēO(xJasVVd}i87]349̄poIpPFQV2S)\}*s:sh|_!!ɒ<,cpUvrCwp ;lSk iUOӣQN[u(r>V@Άt2h<]a;j(FOQfk 9 ڏ{)>l4]hTW{zoNiԓd6a_YLѱ 6 \Jak,[!4:&CWhгuMnӃ4mŔ "d9IAhMjeeT8wv˸ =[ߠзҥBrfꊗu5kײfmg84Ad0T`0 m/nOH'!!c3BCY^).j>vk)5\rcFh(3,Le9,q[ӞvO"iMPЏAMzQZ//jW"""(9r/ҕoQ;J# z0v.O @ \{telcvZs˙9ān7M⧪nyDj .Kt|6~ز4۴\iO0`uGW\&1d!th-!#w(pu0ܦqk1/C$8]'koo__'@ 7h ׶j#<˦*s S6{IPMhC$+ym 1P쟨L%|/NJ,`9݅oC:2v#W/ހ@ @5i @ @ AOf7m@ @ @ 1R @ @  )@ @ a@ @ } @ @ @ @ @ D@ @ @ 1R @ @  흴b 0@J.\x.6pQ6q9.b" @ @ bB8 G 6 6 vP8gaӓÇ3x`Z444?sI=e9:`舍IӋ`sg+;tIwucz?w?UB CWe@ m6쌍j#u\a?} {{{&OLmm ǏNP(pq¨Qpppd߾}յI7b$~8ǛܰإP4&{J7ή7Y}W,Z+{2tkQ,}zia\($%(lXjrM0|NN}fzL#g#x\VY0IȂ;,< oވ~Nq &h{BO o:׆% HL4Inn3/<¤ZA|[.馛X.K',NӉO[8%A94e6Krbo2ýJt;DO #dĸHh7Lb[I ]\}ϫ+!qp{^Gǟ+XV"o%V<1spmGwy̍gY{=/+v.xU*~W;~U5Yב鰸E  k03l话5֯L2/\] gj:>߳mȸo%!#zw8/>ƉS-sڠ'LTWtb(s2q2Rg 2%F' y1,xݛ9X^Q(0T" C ϻ㻍kBdm:rVuDvo͠l+踰-h~`_oİ 1|>(zknjd[ {5d1F:y F f;9T& =ۦ[ӥw7 3} J*~? cDI;z)»S^XFlc?W<LI&WTp-pU=gKr>1N>rQ`1,'mk151R!QqٳxsUyo!=jJv@\GAUؽ|fqf6ucci%W|v@o}le*؜Y s3F"Sqj0s՞^Ⱥ]kE_PMC0\1 $Lr9ecNyP55g=7sf=tI˨/ rq6&@92KR Q{9%K)䂃)6,cl IDAT(1|gš7}ՀA!=,◈\84vGCSKtw>lydǘgN cE1rױclv4ࠤ a'O'gq9{sl>S&ڳ:3w?" @y;Ա8'Qf[S'&-Ca"|"EG%n^/Ql8#&Ukء(2B=$\}S]a==HNo^3r@gHv?-G3{dHI0 wKsG0y$~XGro /|T i&.c >7Tqj>Z.p&7趥r{~]Eq$0<3[,6\_w$ nb**|N[wк,GBRvUߞcnH +(ؐLWnH~l'԰p=THvȅi,,>v"2?>=סbiE;! IeKJI4vSp|R8"ݳǸ#E3lRs1Jؼ +UwC5)x|PxOsC))ڱWv6/< ?e1PT*qH23-;XHh #,P$cv+)[\) y$XmOp¦ip(QY ѕQ +m7bם&$bn@2b?IqЫC{Nź5Cғ)&\dkciKcDћeQf9*wt{| sF^x&Gte`Cr 1+ɿ#9ֿY[5,qFc79R:'{/8Π{%)HnDoow+_vP.Yv%UM+^KW_;I.nHYodUF&śϖR-T4LK恖;mIP:/*Nqxgn8HaIj _zɩ lL"'.\ά]$l$5Ohnm'=a+52ҘG[0LJ:Ag+K:s'͊\}&s]vk˕~4wwCщw]&(QU:;! fuU lBcDΙmŲFB3#:b3[c-M.i'LzIzֽ^@Qq<Db3F\X%7a)Ψ'|s+#9~T|KR yr?D<_| ܃`&>l G$I|@Fn\R3 >߽E2@BR`&{9R٩h]+_;ﭮV`P Yw沙?:Hx͝ױU,T>.1fqc|ISWs`ܽI4_BfK(խ<齠P^R6C;c0?{ndW෈?S~cC%JeZϲ^O_^ "WYlXkx77;SXT)%yryYLe#95W9ىKr~6QMyakbb?d|nxsdv5`ajq;u: 7L*ɓy,:J8[qג=Z7f=(~6XLFIgyrR̾0p{W{{i]~3D_aˈŋ;#y*n'8ǂ0 bI:%"~} >0!mXnd@d1b 6-9SU,b6cej+ܹu+#Gd 8߲W"ָ_zg3=9z dv0.9t.+% )g,eTw1s;7 X+f ҈}x)8վH>Ĉpg*,`A}1F63*_HZ׸L۱;*sP}{TVi0^ꝙu9ב"KV:7nH`㌽ fc ʌFJqp!HL{ 8U%˳wܨò0w2-2ɀ]'n$tgf\47݅ +ӣe vQto brm.CF8*++*L[G;}({PJU5^+48SaV: U@ʃ16آ/c )禁g0lyfn?Kk^E=㘢=g+C}Y?G*2Ńr_k\̈́'F6JPY $p<GϓOǖ @&ͣ_/x]׼lMd"#0qj N_̼ yD򻸜hKײUˬp(jZRO:fOFi94P9uwz;^_`ErU}qsU卲I3-Qcd}e6kyP%M])7Uunxe9#7Fq>40c1=?6hmjpV(G܄ZM];{@-3\`M8Aah8́} MG/d=_ED w|Ƿ$D*rek r+yF4}\%HxX3fC$X3/4q]$F.?;_|(ee\c0j'zc`ϿwsxV0JJ]]mdǟ=OYڟ~>t^ƚK={0w,#A$>-bDWZi%RUfyI9$5\q__Xc+;l.&-]K![o&M˙^̒ =Jd'kGbZ0L =}9\o`G+g.E*84,{'&Zjh.qwJqXz *&0ΪWvW=Qw1nP-*9~of_6<~Æor|R_ ebȧZO3(w,}9Anv(m[gM&h|SWbL ް 9ebM:21XrdEW/ WNfe;n$=jmOn%${ZR>FKzK@\P/.O{`1O}@@~(6},\Ǟ<<9ڡY_ ȅct5-0dٱ1$-dtw`Mߵy$?[iZ%mwFD)nS- Ķ/1rlh9٧Qy{"vUD۾|<VBes9?n-[?fz߳]3b_hj] 7thف4He髱ZesV{zh{=#-܍*oȔ}u #NB5Pid,+zw{;Q:Q.ubͫ~u,XuJsxw8tS-*)2gnML]:ѯ| t&?,4t:BrCc"ş/`c*Рз$WnV.FfA7U$u)=F,ˠj QR^ CQ[E\Ծ3*i;TphrBEy[Lͦl#;~u1fNs7ׯZf|:̚.Qa{iTT=O!a aԘ_!4ku!mP9IT+ gC:d4kX\L٘ BƘrT̡6)(0=G!'Lek|x#[ON`o=ڍ0 Y8@W`Dn;MrlGg~|f;ilυ|}\zeRr)ߗsdϓӍǚn=G;~(7%YKLz>Խ<+odᓞ۸J>Pȝ6w1iኞ-l9@싳lzdQJw4C=njGj*`rҠiߜM(Ocy eij\DPz +T~}Lظ?Ճ}5ѡ$P81ڭ 15҆0(.<iP*U;ݍ!V6n< vv0nnf>[b{8.][xll/SDmI[RƏQ Lc_:G6L)Ƈ0KmtCX`M6(/)OoeS9jn}oz ٵD?Kc'z6l5Ӛ. Mf^e9k7宐̓ zuvrCwJastMSu5"ih#XxOiuN߳]*5Ӛ?x*4|%j?r9/3aRq# o|Q?ߎ;N6[/ ouPi=Q>lP"ўdtSO£%ʕq{4cvHFWtש6E3ܵC6o,.F?c=/1+Vyq0W<퍥AeY3Mɯ0`(]əiwBXʵ{ػυDwW!k9 as2Tb<=ņ \/gΜaȑs8rHΜi;%Js)z,yG0$0žgy|WkiC|+x\uUdJ6e%TScGR1>NԲdM;:p\GH]Mƺ|)6x/=Wv~8UG8v()eqaLb /}:+ym_ {.wfMӏI&#~2rL#Ua[xyrMӺչ3sb-#dt'k- D<ήSrJź[Uu)!z2ݻ̤|r%ˆ{d>$is;)\IE:>@It%<"DcE يˑR %{Lt|2=7d&wvbf|m<7 ꆄ &_Ҙihh@Pp+rROrMo!aqȑ,+[}y|r"  ɍ|=FΛCeL6N qd.ħO63zS|?˱Q3VSǷ51z6$ ˺|_hU'D~1AOz ,|.^lsȚnz `2R&5Աc?bRɘNYagdˢHL %uϷZyVeg˲1==(UTVAl"gLɳ9m8q/wYokO^ :)d;*&˦HDvxzL+ȜKD`~m * Yݓg3RxsX4MԠ5,[;2XGm eVY 9Ħdlnj-;rב>GKUTc2, IDATV9݈^B ,^h+1@26/A/0m|us)>'v׺t0jD0Uou [,k2X ^yLlts"G/V+ҕ߆W^gSL2*٥& 3HF[Gm} "I BI$Y2GO Uď5A, 2IkJ]og]2۟ 7f"WSژxEzٜ65Ed&o!(~\N1gY3n9nP3=yr)Z?ZZ򩭤o 'x2JXf&E;Hs`ˉ2;?5DHdA\ uAVAmZUl>VVm.TKݕآRAe-%`&eLf$B$3Y?`fι{L{)]Ų;Z|@<1`whm>ab?e3u yٞx3f_Z~h9e^|O_:`]s(Y ΍+x9%X\UxppCCyURYNlW'#}EY5c1\FiIKYo;:+.kJ*r$ٹhFԸۣ;kwzbmgѵtg|lfTUUs6"|UR@D/ؿ? >|8 Ұj+l\=cpjh~ޛXqwM\soXo,~o|T޸Dwm`zj;^=1""0LDv!)>v)""q9P]ml횋h4!CO`ӦM8 jXI0kw""8 {Ӓg)t5K[Ըۣ;kwzbgED"d1X ,V01ix 'qURD$FAHLLh|CwjD'Emz@ۅnotiI{޳Kw:Ww-zj;^=1p""j2RDDDDDDDDD$\zy"""""""""ң))"""""""""BHJFHP2RDDDDDDDDD:""""""""")Ndt SS:#2i,[. uW;'w\\DvndN==$SWDDDDDDDD0/bO9SXf0Iqu`ɛ_$)% ?˖|0:NO%}Gw,`G q!7VW4y 1~7S'EVx7_y&1n8h'wUb88ۗ+i:S_ <`#9v|bil z=x׍Oj8V%1Kg0bd]K ݖK䙜,+ǒ$ۇ3g3b`3DVb*  'KۈH7Ȳ2(((hRVVeee-nI7[7Yu\؟goV3-e,],xJRV} U|v,z|&R~7'՟] 2틗X<{?˛pʵ&'h%c0*=V8fnBzx e檹2.nHgJ #;I7z= [4N[DDDDDDDD Ð2uyZu7Pjk2b'[M;ym'q= 8<#YsbW """""""""} gN-]Fmm#Gm9jQF6rԶmm#Gm9jioGM8;ለ4f}jW """""""""}d2uu """""""""h5mJFH0wu0p\L"Κ@}-}5`=}~.)(<;#l+ `7dMQWB?x_Զaa9?,+!GYMmnFc5FWsB{RqM}x6k ն?)ߙO.8#=i¢1:JꄫG#[M}Y4m[eg=*:4k)QEYV_&}ZbYLHEˏֵkm՗u17>Mc&`- HwFs^屟cOfbn LÙuYZ2y>ʆs+6?0y"3%K:b w3ݶcO6,R<ϻ՗pDڶ_.QDKmm;@.m6ױ)es{NmS|xW}Yx4m[e.G1[ikƫ/ -eeSχO?܇֒5*FԗEm(;fJwFVq8|7@@+ѳ?\-gpcٜ7'm:g͡vRY9/We`OlxQlXD㯿Wq캧ԶgΔլu. sSնWM~Q7]zt1&n}n!Y)M؆Զm+%,^1P_.M>po}ǷBSڌ0jҶ£[ո]-W՗]s0hs /  WUp'V_͵p5sQۆmo=ʫz_@!0^ma-|>uN3ea,BԗE2 KRYymp'&Pyz=&x<\pmGլcr¥̮vؤ -aKϫmB}Y/ e,<&Wv0ߜTndW՗Is2/> b{p6g=e/ fv21{(a׵҆=/3_>zސ3'rhWRF6rԶmm#Gm9jQF6rԶm䴷m{0mNa|]ƣ_uu """""""""lc>@sFHP2RDDD$guu"]oDDDDz4%#EDDDDDDDDS()"""""""""BH@DDDz!?r5@o@sp3 ㎻Boݻ{``|皶p ` r>0'Ot&"""ag2HJM Z^QEBrb'Dq*^Ii5U$$%&{y% !lfLQ$%eN\UrtVk4~؄aLWWqa۞H{))"""ji`050Y,ŵk*Ð BVcJtll^l]H +UWö-P2RDDD"tr^蜠:x:ڱ-M[wGtw@"""݉"""v>[ii~?挴=ڮ ⰅR;|.l틿{hMv8hHZ@UyYk8gH՚ Μȡ[6K{m+MDDz73WU `Ж +er0z^ݶ C7@N؆ + @Ekbs׃ҵV::^4W_u1m䴫m1&"n愛7'A[D%e݈b9QᎭGJP ֶ쵓;'h9ǑOvBD""}!5+3"w A~g\7xSU>μNHiX;`݁6rڶr7CH͠e7/벵321Lkڰ eun,qLKD|"__IV|[mj! (o51;ղBX㈙j9<-]?H={\["5iO PZЍx*gb!|>6_~@CqZWυw؆_i( x|4;L5[{@a^=eNgwR}Eи<%԰?s_0:k_/TnO%=.SBLqDjuDML<8xg6Mqv';m"]è+Otbf Q ޻Dkx.PXYΩeSt,1BZC#0 }r ԶѶwa;WR"=n['ZeݍqD[9%Cޕn_[k|vQjmS-Z|Y Џc!j>> Q*o5-VsWRuIW>J+)pZia߇ a4U莍6O;N0線O3 )kjs<;Z O@͖qm=)~!@_#a>W5e{CPqGᖐYbIgsbA{H/7>^wC">JD@ @UU%@9yy0:̭`!ձv8 {挬)柑0npmb;&R7lrUj_%?AlNIC"Ǵ)i4ǭQWwpkQ-WY5m~څgm*?j1CR|M{O]D n'!<~jl5O=v1Td:zvm~Wʦ~W]U/~Z IW24hݒn=Y>O<~nA/"""""ݕXk}׎Jw{ IDAT3d7h GH̔~+Ջ6ͅ㏮=G@̴ 4d(t:^= ?ho\gU Q&fn,ηJIM'qE+b eDM3mc!+Z[XF%}^+e+<8z:J)? wf7!^i^\!_<""""{))"]!@|mBpz!ڪ}67&WWLlǨS1DŽ>HOОՄ5^*hs<:"í} \%_,wċHMs?gO%ٗ=ҹWjxKZP[zj/1g/w3!a%v1ي!ʄJ#}j6 m""""}"e|>7b N ``z⒨(YKm 1ָV%C>I9*[Zj_pPvCZH5gdu#٦sFyS9p_٦z*)P@Yw)}qG-r`jnwy["28@|*vmf [1g~1%v6VeNqwpHtdz?<I*_1U`Gzyߵ\|ʐ=+VԤV7m|[($ơ V͟au|ՍVn1*WCHqLf30a*iLraSZ]ĿkU]m視'ۼ} /CBw֮U >6FZڹM<( 7'j/1ב/cBۆKJ1pVլ]HkDDDD%#E :[Qݟgz_~y_ Wnn?V?v4> .`4a0*m!myh ;9#[QV Icvgtg`0.FwF2c1ƛ p}^l= #-y1#h98c d `tſ !0%#E2`6[(ƨX^k]:]UD\lCkVDu+#niWoGvuEY n0ۇv)ajSiu?8S$:ud>ڲ}M K@aVl?p%nl<2+coo篊xka[<7(lfGo"}djɧh1pئc&xhs]ϗNR?2|"""Y'`4DL|&vq?V Jx̵u /{C}~f=S`8y[[ k02  rߪAz(шul %os1׵:c/½e/ ?@͆d:$:S|\bg|kBq 9^$۶"L¯CZ9{0SF*7տ7{'>u$9e8 C+s?hqdl%֯ipc#>@kp"""adH> H_Vn#~s^o~@OBc1wy^Hx\6&JvOö=@_W}VB3a+ Bٸ=j>93sB[9va6萎56j_8 }8@uI1-lHwnٶ<_9'8B['a+#/Xk;,;56=@ӝmw{jkeh2bXb%*c *>x"Zh,ʊ7E_e^Ф_;bPܦ6) onti{}[1a44jqto/pcJ=D3ƘVfBAρYKjra1CVwWR& x}>(k43aH.d<&cf~ou-],-'L͓^TUPy,l{,Ⱦ̫|^eR\GѦGNucb9,N&q%;~I'E$g5H8))"""}O{~x{%Onh:\XQ7lC)_=8<"\/a1a[[+ ZڰTnpzûn@nc#CYY <GH|~2,Q$ 6Q!n?XFe*C E1Sޅ,(ߤJJs2¬y[U&U-3?欸=aa^w@r+ߜL򐫂-^WI'D%"""d]&#L, I}f]&..{C=~G_b hRs??@L&eNT1/k{3ͅs/#o~pR2RDDD+9+TlnZM!vOa_4[78CGU6)سs-MymT,*mSFt#PԾ  zސf .>*EDR2RDDDZ??8:)Swyy\3lvmR6UPUl'mm:[UKQ?k\\&{k?eXBJ"rZbWk^#! 71!NRlx3r*?_IQt.%#EDDωIMkuՊh>љ=j{bR5)? /(φDY'#4wKtV9T̖}S;D5L[+P[FxGف>um[9 L-/ch`M8>mc~9"""g=mqP{#T<|wA6^&4,nHzgW-I-uR츖W[u)QCnVw,m+s9j+\%!Ao~2t#JFHaC ]C̴]*K0SHvqJ,:&i-qm. 81b%fZzBKxr A\e;)"CHs|n56 DGGGn_^/i G?UEP--8SAbo~5f-[߀H.ĐD%#EDDϩ1v/I,6% gyY0uξNm>mB y sFV_ SpG6]$?\ >lg]'ٖ-]ZPe/ccg7rk(j9!kYIla82pܘIp-ΞIc/G!J^=̚ʝ y<<ex|J+[F\y!+,3BYߝEyd5T!Daڮ}b&NvaD""mC%3>) .:FrFB86t^ V$y)ݱ2r}h/{jId/]@U h210&aʼn@DDDD"|ۇcukыKf]>GUX&M|09غ-f])[Y"02yDYtyu4\D:vG;$RM"ҔyϞAz\4 <$]v7gN9h *dߟ naYX,qZ1*f4Cn0RI+ص1.Qs(%S|Y3L".*k?[F`~^/XN{3f3w<'Bּ2kC7ZxvJ;'5XC x+oq:Ë?<4%/$;oEcdpmߡ>v߹pQf<śy5q:z{n?bX_Ws31$ e&u2:4 co_fux2-g/N;C_ϥYX3\LUf]}!c統7xu{9$=үc;,+S;;oñ~8 sz*5t_y٩$lXx|a΃^Hzw?#mfIJ~d%ӎODDD7j_ĻO% p'x~kGv@T~۹Mp d-5`HR>ጺ2>-2_xXS{dl.3n@-5us@q7j1=6V q=^:mcW9^Ls8FX(ƹCoaT|!J<<]<{uԴyi"/_8dM;L,CPn{ceuK ,~3sBO1S8g+lJN]⩧Se9pۏKg?$ [MB1?jc{ݏ55ٱgŨI{xuôOsOŒ5oQ9+ٳe3&bWW r26=̓ˉ͚OnK n%+%8nQet"*/j=,| [բ ァyKc_;[ΰ8a?V-("""a&W]KNx]+b9ʮ~V8xؿw.r*ϢHݷ-9s.&+ mD?qOR_V^)n}FGh^qY4̋C{׳T#{x~aHL%N ٘;v3qL ;;in[wO(7:&K~y)\sL2su^VSBUױ ӇR(gӚm\v0sp_R.$˰wnzjxz\t <ī>,fIî-S]?' O"")N#Em:ljϹ?#_dO!cK*o 岺WLFdlSkKQqr|񮽬oɛyռkʝo,'$*=>vw[ %Ypyl /_>!W3T5s 9lؖOi0fainvmfsIV+VSGH;yt9Z2ݐl2Y,` .C+<B -'gK|f&>E{yɮC˟:^__s3Ỹ}:vx` &WJ2o/cǵ7po[Wa?yL^w;Cc_FfЕW98b<<ŶB>w*o[][$pa+aN˿vpEƳ <svf2~:w.+""""WY_Cwǽ/*]sٟ\~UvLb5s]$"]F)"rSբ{mp_m>}˚JDDDGOHQv=sݵs?*4M[D6CiTWbJFHP2RDDDDDDDDD"""""""""R-T߆ͭjG3j:Sj:5 """""S&_M!""7ǽqkgecԬxAtx&`b`ty"NYQth Cd$ψ& ɨq866Bӝm ع9 Gq5\TAY~Ϯa؉Pomͼ A/'zGb0aFM{b k-#tQ~G.gK_Zf3h16kB|qs=f%щO)""8%W=dߡDBSO1 beIξHH?wڸ=@Z;y̬lܠD-iww܉J%/e$YMK(L;H8^w Q8SbWSߜzuSRYB#D}U֪;9ٖ]SDjH!Kg0{B^X\G.:MRIXr9^<ZFD6v T}Gv04)SF+hP}EDDD五j'!v aaWOX+w+=֥jZHiܝ@Tv$+)!r2l=Kx{G i̋) Ѥ+88pNMG4Jj# |`P]lռl*"Rm#m/-rjОIKy#I>;j:((w[t#,I@c+m@s$U].v|Z1z)+\\T~xx""rrWaXM!"7x(DDDDGR22;v _*$??[DSAqMɣjY|4^Oia`Nc/Wkp1 -.!s{SfabAo@aou[rl49I'aւxBÎHVu?$aMixqBn[M)φk-(d+/t'(ǟF00 0Fw \-zHY$'s yi˗U@urd KՒD^>QyvhIbJz/a,k[c"H4 ?k\?qՀ /L QۏN;y~O4/lS3xjm6=c0*nX,gۉ\L;gE1OLbePv`;9mX0k+˴ ~$ ff&x `},t=>IDDDDDDDDZohb5.U<5wݍɻ]qLL'E 34oO=wPY2"ЦG_Q7 Gq,ҲkhMޅ %QFx4mxچ4hAFG&9Gi_Ѽ=Y,7xBz \m z( bǥddnb5hleGUP^dtrFeo9&e\37g\-/; ]\Y{ܼ]o`Xlk300({e%U\DDDDDDDDD~:gC:94iR&u]MhlӬ0 `4aSsp4YC[YZ^rK^`Xpw+}}:='Zm\lҘ&ibVL>ҹw =w;.9$'epێ+tO{inFLK!hYҷS/ڞ;YDDDDDDDD'4^T;p+"4x†;e qڑG"J:˖7i7!8׋^;֮8I\Dڝ$&9I&ds6#gb49u^NJ%Dz6:'VWOfm6V;iN+cMdx$ ODDV-{m,ZcVM1xiLō6^Vۆ.jg_`>`dDDDDDDngw@I"ެ328|uϲJO_:6E-^gwqqgЪN.=y )L e#<}Obm&џH{q0?]z.VOi#D`v?ldDDx ߀ فedW=?EDDDDDDuT8ȯMyJb=p#_= | xzE>ٿ$Ѿ#o3ӒZ:6+m;3 $95N@Լv^tKS~3=dswRߴ.v|Zjߔ+=}PKȀ&r%SAMi*(+Yd… ɁѮ+];Ǹ$sR6 jB1Uvqi,"rK[{+ZHu&cJ+ B85E'v,~<v!}c@>""?%ܿ{еsK۾+WV~ݽ)(6#Ve͂5›S{wfJgTڎsO>/\ϵ5^~=_I,k[c"HNG/ ,wps%qL3,̆N eKZ'Ő{hkSv_$1k735"xwI?l=Y&ft;!(6, τl£0}J6cRE2˞ȅ kY\22fJ.\J)xf"K88sҪ lyذ1 ')dK 8gGNf2d@ݛDZ6/4o>~_> 8qkiEz\h݆{lfIe-۵ǝ ڠ.f% _О:L3b>>TUE^S}\2raq&wh@8~K"/&"=Gd[snf+eM]e7$dpL;3GΕ-pHƽ'ӧDD;9^as(5ޏbY'S0ϛ9a#m;1x:q o&U1 KpC..M.{^Kg6}X,xhϴ˺`sEZĿeuűma OkCNţI<4Ee_?[3d]L_S TL2O^X2+Wr)bWsk`ofAқ. ڀytn=i~%y>Aߖcxڱ]i{.n9y'-6J nc8b?ڥjT9zm +Chhǣ4 G~#ZthkG;LғRvƥ 61Ncmϯ:L_yc """""cRIJjw(D2T""RΟ?Oʕ|Emby ~ԟ x~ieգ)`rx4^Sȡ/IX( 92xMb#?/bJ#‡ b(8RM0֮8I 6-mY㻻;OOJ:;9}Y5ǢH7}oP^<=1 Mf=xw8֟w]DD;1Qdw g{l75ΠZм>le}8Ջ?k N{jONK)i?1ѡ<`$/#yco iq̟\ Ei1z(m]r2w5N%`͂fAnݙ 4Y8ʿ3_xsP!2q1s%h{_60a0=Mn)L{~sLWD.3- aq8ч /<̠"8xO0w27:<9YQ4u+A!=ӠdD0as|POVҺbO͈Cq`1|^ &""?R];SKnt'[`#-,J~Z;){'δ!7VU?M%X4 N2; PHGY̯b֗>|ۼhP/[XZ?\|;I8EӢ+(&D?wƽ?cV#"""ZUb_e9#i@6Lc9; c\Ƹp2—&f|mdƫop3/)P;,\4A+`4PF1p7k2qC蘞Z]!?Q ؜6IsZin"#94ɸgGѱ-_WOME&V쎉$l{V$ͤd3vwY]Lzle+VW~o6'h C+xՃ>3V6lze.?.ĴvÜǺbcca9#3?Xc?#m<3eq2N2a#8ru-)i n%]vNCq}7SDIߢ-'_=*ś>lvj# N:)aM^7?HXiFK؎${Y#q5 (~cRϟ= !;F)wrnįś?}{wREDDG&TF:Iy0bM@Z;{<æq_Z{S.6_Nm Yd7+4Ǩ:;?'#qc.sweѱ^2JoQ|˦`[wV$U ϑrf"Ze&gc\Gh ֻ2OǟP䳟{Arw2Nv'v͒y;‰wL.8ǵ.DD~,3-I9d5cXw|^t ǰy`Pȿ] YXece]L3Hrh=2 ID,MQ/bOM{y~(ё$9i12^3ψ#c |- B ݐS##r{Il y?דpx1wBR,f LKe_GKp &  0NM0Bѩ,$>,7&f0 Ҵ tQHL?w'pٸX\\q|]EDDGꓑ(8W "hv.Z:,W]v.O.s NmG.п1l:'9^rUfqo┟KYLǬ Ntr``FJ1#\4]ޞΨnirMWHOH=fb"A[vi\ܱd3>< Y_ eXZ01ǩQY?.^MYۆ111Z:{ūL[ 6 o29a}65y!}ddzGlT?v}bxrnfa^!c!r0 %b0 #殦Ct Dd͆2} %LNˋ_¸7_!tk˿-3{T򞕛crt268g5l!avr2BڐjJI zZ܁Wz;[ceN68i"""?aմIځd=ӫ`TI#45iHWH_->}b``1(_d|Q)!}Ǯ,ƸH7"F?_/ε#_ Au6Lʇ/OomNf`wm\0Ҫ%?x? C]jרSҀ{ 0zXO%=}Қ>ĸ 8~22V44m;ݻY/ §~[ \ {;G1P\+m ~dcfIyƁ_ۖ{?KJ/oTZ.ػ]ѡyE_Ra'r:ʌ}kְsXOJDl9duҤu~K5} t 9sWgK0k{=khԞuSٲRڙivڴ.]%XɏDǿRȩo I_zY'&2b 3Bڽgٵ)'HnI S8㎽5b/Ҫ >8c ,dL ݎA!IKIы<⋌ߴ=jY+.JpG,UP# I lT.I`rO}bؕen-4ɇNlHBK6U[ekW2Ȥl6ϙ;'<`B6 5MNo"|r?|8{a1Ől&YKxT\fL Hf@?0w{q-68w^H::i,fwTs $+T~6nu-m3g3C7L@u=]Dn ;ѾAs/D'bN,WT op1^\ʞSҢYd'r,QVR.{ؤ緶d=VEq=3btl28lmJ+x c[im@zbXR߻~LL<O3L6&XL`K[+|\Asi7֓{ J#솊ƽ8} )QHf&=yN9'޼lV<ѿ '9ǚ<ͲɻYvQ'ߏdWgM]e^?>ث]\>L+Ñ73kˏﶼ3?\|Q]#&s|_b3G|eܟ.m;-'}(?-.a$)U9?فvYL dufb6832ޝ;٘eBm7sd$` H#ūY ÓlҮT V,ػёTR*w{`=nBKR FnUۥ}~øt2{ѻm`apF8W|Bƹ~<7Y/z7!qee!lǿ*\hh8®u)˳7S|q|)eKKfŸQȡCx/]'00j ZO!0c6/f_1qyҷJ398K#&G{x΍%-AuKb͇>j."""X).kflv'g; o6'onHx)ЃԷWd$\=ڝg(MEt( L߰B\ LTGg1a '0.ؕJ.c]Dj˓oPr""լ^/lI t,W0B<[7qwEms4m ]Mxܧȭ{|\k`}511KΌW5!Tҗyt-#"""rUԧ,e|h DDzX:4-ijY]=6v(F}Γ|$ӷVS\3زoP"5&;&̸,O"aF~ʫ_(!C*LFyR:J1v akkc8tƠpCW(: #ٖߝicׂc`䀘Y~ó a٬J.mGTG\I4G=Ythm#y~W?^ӓҡNX꽕nᬺ gct aڣrs<¬',Y6I?fpv$]ص~.v?? 83H DYwR#\!DuX<[w ()"""?U,dj4@6›S{wfJg$2/c -d@ώӶAFO/Rvƪ,4 fd?vO< <G.^<Q#Irbd}0fDG q7gC_Ka=pw3M%7 $x0:5q0Z1 <ϒk8|w~mwEAьsFU4" XҮ%>L]p?u15(='{.ODdg:| ;}`Eێ3Iʻ 1#eEkLJi| xA  e@,Y9qDQx $|`CСEE;:̟GUPeӈV4G : }x,>Άo$;ݖsRgD8ObHKlFV8FaB(=I+E'ߗi| )u@4݈y)p'tc;/♻(Aɰ s5sXf2ҎkΎ 8>^j$ҞHdzk}ˆA QBII ϲIͫwFjbSa>#V' #B,Cv6Ȅ=Ȉ7QxTy2}z 'XkHfO'p,>SCAQNo0u70ƭ՛l(}(V[lױ&-8> <Nي _s8&̫MףOB?~a ]Bf\ݠ0uP8*'?+ J':lbR:k$D[-ǔX=p^ ޓWAIw}ugZCAt۩`H夬$YvdUZ랾_&de7VcA]4hQ<㑂_F6Um=qg/\ 62;GK,uKø=?3&),L'B!ťY-}!OgL|}-POP-vgKxt<"֪Q2;e<ʟxs2^[˜+;[ӓyZnR-eD >پi]! ^fD:ABqFaP~x̚NʯS8Z V+AD'<$fS[QQ\Ew&ipR\s)~3;6 3Uvr#ظ#?ܔC«c^sXH]**M4%,2HRUw2i9*`y-Y䮜JKS=9mǜ CcCʑc!s$ET5{#yPWHkR_A8p5jDO!CPc 2tID/'B23̙yR+a-ym:fCV [ꪏNܩ`&G-pOQ~^7p粰f-(UA+p'IK\Kavm#Bq ?; JÆYLj9w Pm!ąar5M̠S"I_fB'C0O=H#y=GgYn$w[ Y !>Ol7Xž6K<AB!Yg nr`{>K"R!)xcޠ5{X*HqP\8f>y:VP[B<#V? l{]B!gJRŰn!dڠ(fDžSTrL9l !!"Ĉ(83nhdݛOs;bmb-!B0m!`i !B!ٴB!B!]k| ,B!B!.$3R!B!BqNH2R!B!BqNH2R!B!BqNH2R!B!BqNH2R!B!BqN<_L_ UYW [kFlIuWX'aKKX3IZeC;4Q\h+&Cu1y(z{~[yQ.{;Y$ I]ޣ \cl3z"bv:#OE?27,w!9gXD?zhw;A !8a^BpBrz"f5\Sqm(ӭ؊<1ܗN!Q̹/ECӟ{6znҝ!uFR'(~H1bo#sB$RבCLWI߽ (}yĒb*hCIJt&N!2'4W0ÖyGlڇ(ww]7 wfεU<C̣ky̹ĕvI!ױ/cC$"BsdtnG{D7cA 'UhtB_|葞ߤhgBt3x66ʾ~RR@ͻ|w& zI$-3xllΣh ͵Y Q+'ĞWSU}بNs!kZ(t/Pqں2r?MT^CwQzؿ¶_ uƟ_VŘ1So~_~Hiw%wBqZG(SְrW5zH6{쭡<\&f>u^x(PIø^5};_c˷qnn]{f29L/c8e>6ùgm^%P^/R"Okzh雍,Cj֫{p~ág?Zq3[ 6vt-n/Z>eҺc^1dz!9'Q[no=q#Fl(v mS D$$w/ MB殩E;49#2;uOI!a/d--+o/>k(2ec.w4%  (&BT(FQQ١י@싙G;_|<RKXcY9q)$-##"@N;ͻIğ1޷C ߬!);{=vU |{;Ya l){{1-4culÿGb]Mۿp87pCL;Ub;v.ug,lʛA)uQC#^J9if<6!{ 칪?ջۨړ('QiLyخ1X'20b)ଥVv]/wG3?g=Oܟ}\2[fx;j88&RDUg'_ $:ғ-Ps!7'y~X2r-'lihpǂ.e-o϶< {Ϙ$2]9z?!ㆲm\z}L=IIL K2 `O+}[vn:: 7VhshћQsg?X2!j<'۵yMf>f.<{Dz#]`goQb'1b?;+?/M'B\t ߎB,͠nق=&Pm1N\<2h'9B~:M(:8RHPƜ c :+mǼ Mi&v6Y60UJ͆ehV0NqF ,# a +&?qA:ql(FsCN l!woxx6c!IYƵױ] 6=w8tq-f1Ļ YR4OR(;ʈ`X\y"PSz55>hwgQ: Sn\jbPޗGkܬ8P6.#irIC@; X%-46Ic%/SWah$²ƁDOĠ8RoĐ:I˚njW)s$:ˉ5 M!w=XΣzyu)N,< ­I9Q]U#9;$)BK=;.qX??xkxut{[cO|L:ԛ{\r垴\~R;v=ZٻƥO&ޭ/}uh6=#t \ zqi`mj`wؕ:v<W2򐝽vwu;F7 !LOdd> W%xt4G@CN7dVjp C7}jޮ4۩Z߱=6Ʀ0be{pPrplRjfdU\VյS9]-Bk-7j]~F2RurE$1w1/\GAх`YDZanb|ذiDkuy-b1C@3 OWM]8Չ;nvҪh9>iբN:[{DŽB!ΪcA%*!Xף=?_Тu^DɞH\[q9kn&IAA9Y\NCzuZ <0Blu-D Oۯk,j145t<*"2)8CThG>;4Dh8F@ݱzT:mB@=Kv])'u=Lz^SJ) jj:Nxo+yP1%FTg%kHc ^L'Nt=L{}(P*YNfL"_{? B!=F_#3kݓ k+) Gjdʛ;0gt$͈B9u]d5|^&f8S; cP0%P1Żm ]q5:hPsUw*8XoG1% -prttu'҈;[@ē׭f{ɗO61Č>(6D݌ !8]?xkK3ky—.:3_w,q>[i_cw]m'3udoZCl~C{XAΧ[_㝕w/s28?Y .Cnni{/UB\44}*e71fX+Ks<򩨇a,Rf ORUP-Rs)ߴ>$fS[QQ\Ew&ipRW;W`tFU۬1'1Єr77H.yw:p|ۈY\HxpB=@]!9H}|y4lh/zQo o(ecL`#ε.Ew?Gb3QIykn7IEAlפOf`V-ob PJ#8j<c.Jpy6`ϢڵBa0;i3by-Y䮜JKS=KbnX覆bzo]NݐH&w`[WѤ&܌g X sP g-dfj3=V*U[z߇J͊y,MHdxB+E ٸgHJ~Z뱼s"ոwVYЗ9,$o2{K}:.H9ˎ",E,~AOr\&iin 3ߪm4c gT( PVDU]ǠMԺ/moe;TS`,,m< 'śl8Gg+d!r­~ m'?pg,螄B\s7{zp2u3p(;)&azG=ds>E%8T<{\3N,7txџOaGSR aOmw? lUMXBM 9&jv]o%shL: %$l&Rw,BCȟ:M6o!yqE*͸὿bc$"7ۀ;ݘpS"34p4 ƹ5KBt;jE{p;-~N߸I!0eW@y)zH*旞zP[B!ٴUp}pe@-oU #yڠ(fDžSTrL9ĝK IDAT)  $FGAű=5lQqLoi~6+)g!3R0N '*0u]q27B!aB!~].aB!B!i !B!B!&H!B!B!9s_B%W;!!B!GIH!B!B!9!H!B!B!9!H!B!B!9!H!B!B!9!H!~y/Q} `~1>g^N 3)FOp\oP#()L)(X^_m4f@B!ĹJF߲Sf;ƛ^ety8UBxɊ!|c *>!8gp-/!fD3oŇX3*a)$`~b fz:K޷p헖'bF Q1L!B!?[Iܟl`3ܶn#//Dռ,Bm${(|"˚,5 qʙP55 E}` ^)B!Bߺ=CKUTnwwDg {y ; !نp]cmX0 ^\O,r?urڡ̙5) 嘞0фOh#tRpZ] Q$=CNg-//ƛ`"h"$DH&n7^=Tj7/g%80b&ÿGb]M۵LIc@JPk!)a"BӠpHyL0#ѺCC//pG8c}f1[$).h/_x|{/%[IЄd",ΧO'xG :B !&P{Oh=rϧ ("&v}!s?<:ƣ<%( 8ʗ/-B!.ݛF`8:jXvGc:+x,>z}fZWylZ Lch-ސţoաH`==Z lkPU]֏6coPQM%XzE1gVO'2 [ P+!Ba +& r"mMhCSȝqNJ.(x{{^ A҈c9)SKpBIs쵤~h|N“6dYFъ, *U@AB@b٧Bh'i#spw٩ӨK!WPIM^q*W&VEw[˦g4%z3jB2#6~J؃$wvN}kGz2cHyIq.q,h@Є٘Oel )ʘP!v DgedB!Ŭ{F^_OZߝܬx^ll83Kpn/dc/ωehi6f1rYgE$-{ -ɩLZdDzm cqW[x&ӘWO7[iؗy\EqɁlgŏOfz.]b{:yqII)F pxϛp(FS3;`hrV}%8<?'_] !SũӣIR8=ڛ1 @3 : (C jWb@mp*W%g-ZQ{E?h%*%W2BD?:B_?=Z 裧o{Z$ڿ}̄PK3y {j(jvzvwz}/-ЦyU+#nk?FC{6@i׵DL&T;bbԠKhw{[U;1&kUGO6?+e]׋ZM^)@9* n@D#ukB!s7d\5m, d?/fMNB\뗰p3g^W˻L;_A^bl65_0gy濦1B}&6.eTL͘&Ve| i"B!nҴ,x}7Z‚LT7.fh(Ii|ҬcČy `ryeX,OĠrXyaS+gx)kO޿g8?G=u#ϗ-#xp nK_=]-} 1 <:)?T[xcͻUc kxrF7M` 'G3T iΦى"x6y0PQiŶ~1OnD`4&w$y0iV" P|3Z&BarC!B!;)On[|$#'dB!B y-+}pq<7VvID !B!B Lr13̎a7TNjsqY!B!B!.02L[!~dB!B B!B!BB!B!B=[81!B!B!~gB!B!✐dB!B!✐dB!B!✐dB@A?ȼ(\>k 0DXWgBÜz⣍f ”14 D%wU6~;*!Bq<!8g6/!sUaq#1#gS@2r:q !]F@B!9p whbч 6=^>MQ hv`h;}` Y]<%8+T;U*.G!B^HH UTp$w;{MaНd9gBe n)P!ϐk]IЄd",ΧA!hukKig^vv ľQxXfPFP :&P D$$w/ Mz  (&Bw){{1-4cizUFaeWu(%R)e86wh_Alj;\h a\2YD{vI&#iM#SRH鋢|9YKK$)BqzFcEKU}VBPtvmT2Z)(:>]|L͌#nִEP8MYFFϢ*##/u M-sbހ8Hx_I+DϸF!88BR7u ?EՊ,T(LL!'WFkHGL9?u 2$"ow(IO L;`@ {҈>#94%׫w1M!{w0Fx H?Xw錃 U "oBʀ2=d8}%Is$B!ΊEv^m;yow ʡ,{v. ^JU^^K w͛ʰ+rEsn$; -{>fI2:\IV޺ǧRRz6sI3}NAw; 7k+Wg]؛2e_RX3gLdY%pm+sy-$Κ «m7]Aqpz%7T͌'rٺQ{`9OOoe2o(]ܗ쨁x)Ԅ|_-[ `/0$e%N765ĨV`wg!9U:=z NY8"ϒ:`gLxj8J]䬥rW }Smi@Sa>@Q{:j+Ey9B\l4?w?SI>CBcBhuz?ίW k64ay(ɋ0r@2/{fdU\Vա.B-XF޷"C$cާ>^p+ޗ ȫB!.?LײU ~YĊ~OÓɮ2$UO~Fo3a{*ʰ$^I°MY?= \= o%O+ Jx ¬fk ڻa[N&4Sne:2M70l?<TFn}#W{!4YVY!m1;Ȕfbn~Osɬxm.o, =O@P9)Fz 9/M֏gvq.>?<0Vgۙrh| VqP­xOh;^!TB_ZAo4QC0u6d'( *?vVV{#֛Qy"g!F(*wvB|jK뽗A߲zjD0T5{x}c:^^mV&JSzg1 ,jW?\FSn/]?`t1)HB!Ũi{ٽ[Tn8پwk]ħ#aO'Q?\Ϡ^WYb+3|kVfDIq4&MJcެr>WtmrcJVbU*V<{~'ŠuV| {YǨ{?FղjzmmT4k9oǰ>'vuz+k}A7~W\sTSLcʝ#pܨ}ȍ a{>zCػ!RYm䈅f0ٻʴ_b̽-d ;fi::Qy(F`J0-p M!5DRDsLR1Q ap(A dXJ5 'uu]sZ~g#8Ow{s`OᏎT sK0­;ㆢ9Hɵp>EFE_&{hΩtV@-&-C @1WQhh%-EZNƏ`NAM #ǵ5Aj*YHB^`Cwx((|^݃[_-̡Ϫ[C<^{ߐEieȶJ,4xҩ]k¬Mfd#:+0B!76\}_Ma;Q1iDGuNͶ=24UQ2fZ\jbiiylegeH?pMCF?OlG`eXα}Mw&w{`P_M"aV;N`Gώp̾ŻbC֭RjBJɺVQh|5GwVm$PU:ʢ5Y<-/'yRU=XB吾&>ĮA'?$dK)OtVccgF:ǟ^j-_f.#Xi-vE4 N˼&T_ID6T.0?uJaK|xtk5ԭ{5gTU2j!u'5X~,N#*ua+>X?8cVc !B멃`iG͑lFq3hFAOo^"*?uO;}Lfv5wsw̿㊢`q,c *.?|?|fv:%j.TIJˍ!O*$$ÑM|RQvz)8Q&:B~VvS3H|OCU!MmQokm[%YkCZrJ*_,.&#ޗv/%rEķLd덻"dE|Gr˴(C"UV7S*!S I ❠N$V p_O8%*mn|w7.^*9nm7S!Bq꠯iؚUlIb3԰t]]9#PK!,c$m@BjƦj>wL')%%WzegF0N ucYB*s<ˁ S'*S#†WPIJ9@s` +G[R[(|N(B[p/8xͨݡddJԁuPt[1 ŘC.1h8vED?sH*S`67ycKX g6/d*ͿSndp+G+/Bq3h|Yq2dSAI}tpΉV4۪]5O5}Iז~N䵖B.]Ze?= .P'3oNg+pZC~{6C|7ZHߢ&B&8WroW8 WS1s@,{=,#կ (yf|mnTީ~WcQF Kc rԒ1:ř6B!i FznаK5j'Knpܤ^>\[3% -6"wk.}r6=삾3|y"xҌAnweGط*Rʓx5bYH%'{F0;πf/=3 146tn̝1'5sxo >(Eʖu䛴7 Yu"w!ͧ(VMUPV9O-K}?antQ;B6(DMɬ(US{[i3JԷ4g6t gi%]Ѭ*\h dKط*~ wEOPt9wS%낈l6^GAFhg: i_}B!²[ћj2+MeWRJ5D?dyxiO|7L>y#7O>==sPNx!y /{#el~ŗb9tsw^;e<͌W6rJ8?ɄI^̧ԗ>~byi yѾ<:4?Wxk:Nkou!R[ant7MAJTM ݤde81@ZEs4tb|Fer1YNi1Gr( #ǵp]})Y;YLy/WUTNt4qHF1\ c=R'Y8P_ʡ\A=pXgP)(/wݶ4&F9 U+^d1mYETC!BS=JcG>z4 H^"c_pt=+{T $aߧAU2ȿ+ݼZ\ktPZp~mcFաBy`Υi\[ {C1@u9e Cz5 FBG+YzW6TIeњ,T+GF=;O.pAY/2س{{ĎKg9 {|^,v{7zR} ZGwlir#I.sT&<1#itO̡آrWg?oTP}%ezcz~uSro-{)*"YOQySÓ)4wrH 8Z(&O0R3\|+ZO yB!:dVgk19ХYȞZw;-On/iփXu!~/zS:+͆Ɉ ̉c;urK y.s!#jQS.[]ŅJֆ^GZ+wEr)ڝ 6 ؖ뭀j pS*!.M$g^|A A9ޗKkRTB6X_+%}Y0闥]IVl0Y ;DZwUPX‡Ň z_ӊ\5rڸoxk1]x;ΓPG/{ ֠8[N̷n{@p0xg"pc[)HdKq9 @B!n^t^];;qul)^];˝׽HԑDϚc=ʳ5 aUWT$j{ޯQH@}ڴYL2sc$@T +cֳcҹDyOٝZz ig87휣"RVG)pU[?_·>@_ƶAš`5Wޢîc9w3УQ5RYX@ IN+ӥq`ƺa߹!h*$mc*Ƒ!=-u]_"(hү~Ug7Jő2Nqmܚ!z-vUOԀbP4V^sl ;55hZoEׇs^w휣fLTd,'wϟK&|F>e\GώP)JM(pm5NtGP s(#'Ɇn2ܱo5 kV+f4B`z#}?©H %cѪ7;ՒD9A ~4xo*\heu}9N[w=[ Ⱥi'NfcS%낈ldRPg/=L?Q 6B!/|ҥCzv%ॹNkCZ3gWٺԏ dp5֨UܖM3Pa8W ;V4qF疾L7}XSMBe&ə ;AMV"_M h,`?> E[_ƞ(b>*CECV'P44 בw@,c3Ac,/LNS1[/$+ڡ~D<3mZVel Ŧo⥙l*<s6d c[e{ʨ:`wBB#wP6/\$׉<DŅ *貣$ҥ[`zግYc}@a`"F8B1C(.`B8M«J;N!odžqyy}XPiQ'f"냍5}M(V }<D_ ;׋pµjNU^8\K=~ʜ]ߨsT'[SʱkQoacF10VW4eJP6c"|x I12F5m u?a8϶}m]wu6ߕ0"k(%pTܻk\%&~hrM+yqPs0`C[D&Zk=oR0א^Z y^gC3qq- TX#5׃a 0}KCw,0-hy=R)J[I2TpY*| $ܣ'mTjTת Ig:@~痡3U> vXlv\9<6(}X|*Bf2U$Oe(.o}$O #SNM;>o>+&=c&s .@{U+2g?V}}_< *l9OrV=KQE Ivj]-J*r?!8) z/Mn*p3 8Oz|Fa:=Bx<*OS9J|RR")7 *ıtG%،; 7 THt 0QI+=>tiȾ-]:s(y*Y346b:]K-)ތ*ZӐ!2)d?aGOOX*oRth7"{W}6xQ.'uq$i*ٽ=Hxbǥ sm !B]Y^]{<>Z^}Z |l'?淨yTWz*)jlJ{y|mΠ)L:9'_ĦdIf }Q+m_\IObB3iqCek%=Oʩ&pk^ZU@@hZmNk[p#E=%Ƈk9jS1#E4 "o5?=@L}-&hUۡ\{ Ge]#_]eNC*Gq`;B`g" sťZJF:wEB!W[X>ƙ-]k F2q#!w0qk^Ybbz\:Ŗ!kRPZ d8*ZA\r6^vw`e,M4 DOD¬ >nƏgQM_)8OZY8iG0bCNӈuԬU-_|uRP7ev_n1>|;~Iw0k֦6J]ؿ %U\i0$GVigJa,yd5Gv ;fpmQ'.+YHc I h;w|Ky0,̋uŔS//rujuǶ<浕dB;<"6@:*բ8N`m::*qT~F 76=rt);VB!_>p,ѫkƍ/k_=>sI~5 F;ڋ1 v1 cH{GCtJ@ˆNnsOφcl0X|M]qvúc*$@>YÄB!D:~=8C㗵/r'M>hsHj{LK sA7SRs)a(RAl !eM$;^ f?LT\<7;tx K攑6RɶtO\z."Ԛ2DR'w Ps$lׅ26@&)թ!FZTgy3ąuTFʋL(N$tt7y* ;wĥm+ڰM/38 3oc+&|C*uUWBqD %U=DoόI'vWP/uR(+}s1 $?SLo ?OQM 6ٗ-~z]PSmBp5TlMd܋Hymb"[F ZbZ'{+ZYTBZGBR[LpP)Ǝ.B!ѫ]\՚յ3{@>҃.wrlӟIy,hBZ"Ͻ[}˾ .B!B\/-zFֵl\x/g[b,G=cRE":쨠¨xܠH!B!Fn8`ʩo(Y!DGplU*2YrՓ0!B!7fôB0m!B!7[:B!B!B#B!B!Nv B!B! \!0LXC]%cY%PWϢ 9%jGF!BdBP O<=h:̎s(fӱsAT_uQjl)<|!6 ,5X.[Q axkTѴX^^::-~Z:.e~P+/ Cgo荔7K4.<>{,.ODOƹ ia<RyzA(4p(vlc;^ݹ5y,{:w֚9/:VpØ7|[kL͗3F⤱cY(mj`<#ߤFr*e1< Ql/ 3P"x.s{~*PsY~bӡTeᏸaMZ'S|y~}{d[VadCwk +-ѦzM@a ]YV1}0ʮݨVeedL;nAiAKVkfcX-%b\ESl=n 2ʔ sp!!|D4-?y~}0xenu Imafj<1=  *)7{4O bOW~z+CLlfbjݗyu?dY-V-o+\" ki?sYwme /O;.׭¸Ŏz^j@͑͠&j鰰9=n&TL]srOcْaKǶZ1$aq^M; J;io_./}>0Q!DulG H:bz5ӫ=LKN srXM~ޘAG!B(jN);lJX;X*P+9q; eImWZ9LO{>jI<Ӌ(B/vEUV35g0)įɏN5P}cK+я!\;Z1w,L Cc 'y$M(zg ol9OO7Ձs$fMلhFТiJOaGU]*j 8sHA3X\fs ^<>J;POyaR QMkXV(a:4Ttszg֊k_pBC)gg сmi 1UbkóF;AՎBJ+4F_W*L xZ˛(CW1B>ZN1y\7,':VRaHc1VF]ϵaz3@__o#|)w (ږw:R_oG<{%84\7Hq{OEkcVPQhr[n}zଶŸ2iwP17qșESĦI]-?O?MXOQ"f6( ,VblEٜSk][=`DOe+0d?fWpAoRXpy٪ƎBM'/?z-NwRc/̼լGEuh:9EU_L`[l)jaK w6oNSQ̹c(}Sǖyv>O^?^]U=#k/nᩇ~Jkq$wvV`l̼4&RY/*g^XT+ j]ɣ}䕃eodstZyC{Ld*/d}TݴNORycjƯ`A>k'ran F]wETUu|}..hO鲽 `R?{7>7PZUoRu2~;O]{@&5 G);k, snkB!B!ď ;bߓQZt2^D'L IDAT<(Woxa-?b2O\l!B!@Ǟv`c+gVb`k2qJB\Y-:'&i_}?@B!BqS~s, D.E$\ Mcr˙CPlNР(^ofvfcyVY?w,:~Êy on"vy:~k d ybx% ͫsS"彦K栱D6bSATx%X"bnHmyر z iDOkX`6]ؘF }͌$;1qu$muH K"H,H`FLK"A*^ IrРg_' o'\LG/z27 XJ]ܴcME) x·IM"n낂nrƙSpPWBnz6T8B\M'籫cC1~:)ﵴr8-[AFg8{u19+r\T!Dy!?6J\ )LGUPph OJUD+064b]Fq~ߪ4*j3~zSj]&Ңb Ÿ4,y  ܹ;K DU[.I;RY<+c?} 4$(H3KIzp?e&IPcc1!$@ݩ~DUnZT&T['5G0qEQL5iID%o==S!$<ˎrh7`C`+儂Tq5-sg~xJ$wYvqY]4"8t)TS~@{7uA4a#dXQ-QZ-BrjxI TKl|V1ζX\ӄ&4̅.ORۥWBaBq5/0mJڰ.B004swhI[jUu Ӡljlb6Q}z]9kKPGm%յ߳jGmn҂]e| lG ,ίTq5Eo)Ǧ5khP T;/G> `B6DVEu ZR<(S{& 5Icj)~J"6c9ʧ`- ms4[(gW{/.ܥHS㏯:l{ʩCAmc@ETPmP3*_z%?)W*OgQWEIQ֐[+hŒ`$WPM x }zP4/0DQЈyәa.:BC7x' r[rS&+Q5,~h9lw3zb3z?~Mn*v"G4RZ9;\Âu7I),s5+Jžy%y *(c1)H"wBaYjJA`έ]LT+[~eq <=j-X^dnruP#zfGk y~H)灛z32ULr#MP":'OUʘFr8?7r{gE $)Mì{8B@3|(UɩFo%o^.sD!RR yun]w9 e;>dy$]9)e$v(D'%+ dv&>cj,ە=~ ntUƌݬ:yv Ԣ/!{i6264*"><xBc9ZA&ɩߧهaZwL e ,}fW c6}&ky?6R%knjtWt4TVQ+chP+`3B+#*!OνXQ ޖD2)ĊtMb$KX\Մ⫅.{/(_L!6 K0~o4zz&n+"i/^Ć~W98̕(4hq6bJO۷믶da>ydcqT4똲pi^ƈ9ıJiCI\4R>&X[D@+ބ34ՠdI$ OH_q|ݽa^IRǒGdq*E-458Zsg*1>cE@ 14,N|$r 1ʦbH )iذ@1sV2{?Lʖr/[:x>ؑFh* g>WiV')Є;Ea23** L1>"+6|'֖S+I?s= {9,J6.ބN nW0;9¶w%M>j8Ob(?'6M^KD2m#@QF´s?J[]m3j˝j)uxȁ˰m+"f2"MȥFGM!W:ލ.ꂢgޝyq&ބ&31RRb]#0G417 3LC̓m )T4k 7`%^p׋&R fxzl\IZl%]ܷ@1buW<Ɂu?ŒCkwVf7Š#[w30אk o3B+#⪧bەO|ccz23`h-hG<e\A_^uzص &e?!3\m_-xHe$1jtmzG~hm@F~7?T6PS6C]9 5Y$]?qs/Syb, PVP|=@3f,#hh%4Zt6 t{ӆyE KI{눘imEBK<#$+eǶl{r%3xSケ_ 7a]CJ#q{zswe^I.!!ɻ< Z/ ):VdJa߂975g$ 1;L|YF?UTUw<=ή%|P0oJM7~ ?͒'P$}џ{瓔F4*=Kǫ9ЄZ´m*ڧQ~9"W^[nd& ,X6L,4\_Uq"jR뺖\6]=n,5uжm+yЯpd/ ` rS]B2V0oz*6PYTDi]+lf !#da ņu̟d3Y;,^4_uZ  aϮ#Lfe~鎌g,S?\!drV0+։o+Hz_5g7Q,[Ui&J=!0`"A8E@홯4U(vkVقL{tB\}T_W˹m^;O}S/࡜^V'o1]V̕'q-h .;8.ϝwRv_UQH{=}'2Bg+CbX~vySC=WsW+˩&jL9T(}KƜ]}.)ϧ#JK&rnod<]0+D<, JsY\z9 E3k~!8gA״"?9|fLI'*Ű^ gbxL* Sl?%CEFY' +ܺwB %0Ѻ\joqPi*L h{r!XGExzl4[b$P2ʄ{bзƫ㻃JEL!(Fc Dͤ]Qso[F( um[/wG{:'qmu#1s$H)b9"3pN"2LuKZOIF*t^`RFX: 7S|h$?sY@}brg-m=LL1r-7^B^Æo ơ?'4f]\EW|(QECN;Q >U̞^#?ELid eرAHH!NmAB3(#$%JUA56Z(\$9▮cQ'ՎŔOVv/Rv S>8(sۏ"6|F< }{-MФhfx9(ay#8eS$aN&3z:NpLދyEVe&`|)DU@!KW3dPej_lb6`#/+4򧁽 -\X.O *ůD-p# > b q,^O+HsȡϳG/Vt>VHzɽJ座E/4?k4Q&iv79НAz ׶ (E4)[ubcޜF6u̸M3J{JkyDf^wS@GԥRCaf5n_X(>7]XgN ·"`;"NbXM,;73)H7Z+@򼶺~Gw#:ևf3ED2Go'|뚺+w?8'F[25ɽ&Dss!O7$a.acU>ŋo%47Nj=Gh*[=G8vdaB}AmS;E.Bqya03h0o 䞍k9ߓ?t}R8?oVQwpm),2LFDW:;1T‡]I[1_%{B?a}ܜĭ:fe?&y{d-[˾ù'eUEyrpR/٤a'dc1 iȑȮ&'衶{l?>$>0mxoe'4el@7rxnz5罟eVG3>,y&t?<;6*,O!@dћz(JR}X$PWʹL^"Jrհ팕d&nrgE!~r™@ʣHI>95oIޒ"-q$œÐ߾h.;RIOH0lù'+[I_I05:N΍k)Hl m=f>qٴx Zܿ]*4'7cXɈɩzCpu:"퉮{X4.E0/F29\ԇ$dNǾ!fÁOX*>˞?[9,ܥj:!B+V*ک\_Wƪ- 0POnLw!B*{K ʨ@4E(\& !Niqf`†<(u2sקڑ8kO@a'G$%6|G ĵawFH}Ƚ72Fbj91f#Ǿk_[IzhI}u:{eSm1Fn?k~>眗9@5UE.9>칈wB!~((DžUTz9Y=Y|'H!S/w&zCŴ,v\A}?/ol*z7]!DܳqoɆjF ơ?o?xXOnsrͳ'GׁJ{#ۖ{z0;xv\UZ%<5+ὃ*.}QX~J|ީRM{OV7}s$Î=gqi$2'$B;GiI9;B! \er$u' f@g8vž ӵ,aʾ]{˭UUgrSU|ضރ M5~&|*pJE=_صzLۤgsb5gOK=rhY&o>aWmu|O5T@߇=d} #oey !B!Bcr0̈́ u>9uڧ{D]veKW]G? ;WS/c5O$:Sž;۹E;R*4YF!B!B a'cX/v[{@ ̌k;inw:GrB!.)YM[!B!ĕCHgG;(C4|Gحc"B!B!W~Oިz IDATM`Dy ioc!B!B!DaB!~ZdB!B+I˝!B!B!ABqfĴ$2s[W@66,.bP"$K3 06.h]Y, #wU%S/mGU 暦58ZV5_փUJ"p3(B!y5m!?6 U`Ϣ8}ˉvzh 1Ei Jf\,Fr}D٘NtNDh3jGP#PpYYBzfzLLϾ`| YS(6h@e3iC 3Lz'4Sh 4@$(pt;PG =: Rfbb)`v m2s]yi 3m,&91/e-q)s5\8=1E. ;ub ^<84jM.ֆ}xo!|.{OvC?L$9{Go}+P{&ISIboǺf-1lP4[9:sGA8;߁9%*jm\A4WR,B!9ô(d;G3ʯ}{N;GuSC}dF!!)QC kmAW--*M<)m^ HOov;ߜZO^D-1F>P9Zk(1L/`_&ml35},G%6G{q ǎp +(\F` R#!-8; ̠*Hx6(ULNJ jp[E}8T;S>Y%̭J%2&0O~el?؊ik74RɞCUk(|>} XC/3̣mȸWƗGuKKP XT2,ֺ)z=wM#p[)۫dqih#Kk΍B!Б/9ml †p;Čc(-3V_,(#;K!.Oݧ{.w6B!Bଞ1iXkvO:Qiswol߅D=fCY9S6gy3˂v|fޖe<3}.au<94YNdg3%<󈁙sc}x|N3G]@k-;֦q_l MLcmmP̉cS+sB!B!鴚Pc(?& H>+JX;X*Vs%G@ac[u@EJ*UhNe j]~5Xh0z Z|&8@gm*=qn*-nΑ*B!B!:bgjnֳ=9Q}Nȡ> aC|)}>]*NWī:.[l٥c]޵0pg|**NN=9B!B!BK iuPP߳} Ρvp|Znh1~S9>SfpK@[Ki O=Jlў~ F08_ib?ݟ#B!B!d}:WPp{ڧ{DNeW.^LZcnsRw`y<$Yؼ4kj[!ERû2Z_gsOЈۓ8ڭ5*F*sB!B!ƿ ;bߓQZtv?[y{v nZM{Z/B\-#d;B!.7oFLK"37u#H:BR~-[)ܔG(t 3;g($$&/taݧ!'qLIx_P.B!ui !QNN1`:E?lm_-I~z&_=zV.{eĦ*:BMgQjubiID%o6׆ $w.Y@ߪ4*j]+:p";~LTn^Mw9{ hq-?HB!շ˝!ߨx~ڠڰ0G >}F ՍAu VLTy;G')hB:mG8B[jUukUjL&y}[+GU jGmn҂ʻB!1BqkxA{%XNCK$a( LkXU4bI~0+&rts#BCBdN?@agIf Htj21AZ<-d=OEO#$ }XC&p#)Su$7V=f9|yy3yčŧJ5,{zb2aaQaP->ٲ ɷ2Dr2~țO|Z=\N544[9dAL dBz&NDVBc9 ^йj7xbُ&r3[Z/p,b|*]Hpd`\[>MlYW*+khB!ď#*!OνXQ ޖD2)ĊtMb$KX\Մ⫅.{X)_L!6 K0 FWVTQdmE$mAN'4nW~)xa7G7sDniKDžl,SV1t '8֐@ 6m(K&ZCG{T@sH rfl~繼=#2r,~Ky *e 1v[ۓ%4{'{^e !Bcy>m c eS޲>-'XW2Ts>#Kw:{œ&s. ;tH|aǚbrW[/oI{^'᫴wrG)w;ݕAXBRE3a,Y& s'ӟOei,y)RF 7o, ߜYȽ7W{6P\2њh'1>}G-K<)I/?g%ofs3 SpAy>'p9״R[qXXRRuUiX+T_K;GT3#sE)GiԽ5 3P,/-r"V&|.C5_NSv6Fʋ^Hɨj6-hS x^`|@P[2op^cclnϝUl-TJXH{!rt<2nHRTC{)|ŊOWB!cEƴ1DMV?O> :-7~ooݟ/g٦KZ|~8TνT&]ex+;} ȟn~3poWDqK3Q~Cu4V&3 1י{m>wᑇYəcoȞ1?3q<ěu#wHU*?-&u6όmcn| ;h2Ͼ\x uKv\f!B!@۴;otgae?jDKL]Lȥ D^rC`8N,hCs+}AwPYo=nB0yOճf{{|X0e=>2Lf2wYd.v͡_}?'+z ZMpqP4ٰicU8B!B!&]i:[nƒgvv>MOtdIx۵fAfv1lRcVȶ-陋wgĪY#*Sa ZZ-|c˗qP>bx.sy| G=ŏݿorcnf&'M3GN]<Gq8帷1b%\fR[ۀ!? '2'z.G- 嵾|^T;ͣxK@_ƹ 08VwK٬;$3^ۺ?W Me)X@{ϛh~`~'<4Oտ.v7$m8ȀIk~ZmB!B!Ԇ; ޲OdO'⬉,5d瑗6m@*_A1w2n+YhR?M4Dgd/~Zz, _Jc~~L1>i;.~Y,ȫԧW]#}H7&һgNfQ _#?@Zjݷ+idK)2,g[zG<K諏s>o'37#}!~VZk2&3iOB0bC2D3ົڮŷm/y+JpTJM4 f)WB!B!r{a]&F_m쯜O a<ߌ&hvav Ҫ#]^r[qʤE˟xϝi8q>&)J4/qu\Cc;.?f*Hx~fP[BB!B!mG|ܷw⬉iW02F_n6Gdl澽Ge'kQ<*[OspS|ꕹe;'0Vz^+5y]sॠE{7/.ee?#Ⱥ$8XQ~췜sܦLdܔKzuGNcG;w46u7.YiG7M3aGw@ì:8o|,{du,Ͼ9?`-'xO}9a9)'ݒ)B!Bq\t>9Nv ;saCYV[nf׶3{Ca</]v{{2;uTTU*7qt\SK:W~Mwn,q>vĉL ͹O9#껹?o;9d~ dw ҩxCPYxo;T{i3,TuS3s7o=@փ[n9B}4C@^A@a\r>zv!B!Bq)5?Ag ]ÖL mhs _O{U IDAT^XC+@E_Iso]e6Y5YYKIjA{ Ka*>@,̨xȆg2Xz4p#m; ZՋhw99x2{l쯜{Y(}ƾ/~3sΎMoJ7rH*{.'ݴ0|?' vZ7YPW 3eEdCa1i|;zde!`Ep<%O`+ӲB!B!ĠcŘaޤOØaCYѕy_hBw3X+`qܼ+K !B!Wfd`[g?97CiO5ֿ~v:\(enk&H!B!B!.s=<)=YFmyo_*.oa~|j~TṪ}.B!B!x=nBe`i?ϧ uԏM\Tֈi4  Pߩ*LŘw5vKS2~VgdcCVB!y6m!ʩ;Md<̣a>SQ߆ 2Րr`,`̝ڦdH% e Dc,3&o'ܐNymDG 2Z ﻾Sc;Cdr X_=hkQq-CV?3`7I,  Y=5ߗ vG.9~gN*T ϷOj[ u>g0iC{k6%izv>\| ؊cIJ]>DƄoR D !B\$)W=mel+CA܏  2i}S&Gn@3r(Sb 6w G[b2_@r{|c9NB#٨jJl<Ղm]ـ{^9a8Lf}{̹vD#: :~{]ulN`Z`%M ̘Pc 3&ٴmPU!B|5I0R!Dp-KMΔ@_`){lx޻ETpRz:z=bxϗPOy%ٗۢO@ Q#xg:MEm q gʹYKld,%4_ 9k˓e}{l}Lר힞XBՖ$NBC,cTaH!B{%y\5`6iK[Wlr21J{kڒG+?+@CC?zJžJu>IbRs{ fLlf/j6U^`#<Ϸ o+el*U*65( "y E !B\k|ȷ}!sTJ7CD@lGc Mso,B!mB! "nڵTo@B!`B!&&f+!BU i !0M[!B!WdF !B!B!#B!B!BB!B!BA!H!B!B!Ġ`B!B!bPH0R!B!B1($)B!B!CtB\i>yE?oqZWWRIKPMKksv.keFO\\ʌlcS6ie?h kh!rԾ @3SH9)W_B!*$H!)я{dVA Dv:TAwH*p>Pn#Rrrf8RfP/m]B!B!z۴*l;~j -tÿP9zY||  !B!"Bqk{s U8u[%|AhZ+0REiLI&8& SPZ1>lxizǏ@(5K)~$ 9)d2ӸH֪Fo#ٌE9REIa 7$[`xINYTqf3mLxZ,//|O%,%YA] 'lldJT̶K)y8v f>#Fq ^Bc&h %İ(CQ?*@!B\$)W9uyt"L3qDzu*ޙAƸ2R@,\t2ַBHxe]?pC:QaxuTK $fP^P4Sk F*0*N9X[1$F`}4SF}sU9BQdtizHqփ4>TcFY1(LͣΥ%2!9TKFWDZX6) ca-H J ̀Bp0de%c;adVB ("B!ĀI0R!z*-(V2:0dL/ȏ_f0#Qq%m>d>Z2G9ryH%iu,"8W-\(Gh<І֢CpB\VfG sDXAn Qc|qhs uZ7=P&}<T/M8B ?IY|f]+lTtM3}nQB!b$)z&_KgJ/q~W{4s{Q]VvN^Ox[P/K:"ۢAѺ>g@HQPn{/0/;!"<%/ovԾbXFC5DD1 >Ui?|`ΏdM+k2R"s2ƾMK(JhIZJ?!B_C+!ǘWB!B! i !B!B!#B!B!BB!B!BA!H!B!B!Ġ`B!B!bPH0R!B!B1($)B!B!B!B!B F !B!B!#B!B!bȕnB+͇9pzM;N J*i9} ɀyI~m.e,}H+ZM]u,|@vEh 3kc > Mm(!mdy|#٩f.ŵG!B|%I0R!rJ%YzUPpiUm(N=h"/B@9@J#&쨀qz_-AD -r!Ĺl/p\kB!J۴*l;~-8_ Ie1qL wehqpZh8 N塞]q.ٱZy9B!+A2#*:Z,"'pKK ( XW`|ӠLCqLbi c}VďQlkRIrR!dqUF拳3Rr=uν:in$ 47+{DCtǴ Y6κ@w}fLEflGB!Œ`B\ԝ&2o0i{שoC{gjI9NI0xNrm|2_[߆2B "1vjBv D}FQq/1PAyA)La5;`mn1c>Df.ǐڳpI)$LH7yeyj),,U4SmQn#_}`!++ٝ%D3 mAсfc~6/2AaVB ( W(), }ѳY#(}(ctmt?80.5~ZOg .#knF !BKHBqSiVF2؀ G0e~A~64#2%Џ+)wϱm||}ȵer8J&Y;E>#p^=[蹾őa(-TM+>N#bxdݣL쳝/!<%PqTsfQD au0;\KʺFvGQ'԰ϕ7%Shڨ~5$Ogm+[8L xehѵ:Z.B!N$)z&_KgJ/q~W=QPzFu"Zv\_8z=m {Tzσ/;h6)04ϿB| x, 3يz22!2&iwda-ҳ(wT1S'3*!Ie=wk^7:u/lk6byYO(3* &5y X%y9ASL :Ia/#Y.'')Ui4|ps^cLJt+(N]XAzLοAɔ*&!LI&R,)xA>W#J*$3s70B=O<,yq}π ߩed9Mo(nߣɔ.pEaXrbϮ+dS@I˨xg=5#^xURl{4z MZ(}Hrd{DaȞmHqi hnͦpAh}# *W8 a1-w͔?߭ aRZ2#Kq"m$yY)iS<4\$c2%-1O1ġR|sZdt)sqz,ͪRrnM( Ř~Wb<}f攞9,,*MG32b̫,X֬r3=]G&)|: 3]O]>]ǠHkikb 9jlz*TIygM5\&-<^<v_v.kD ŘKWn1Sts>(?V!t3\J`~1Q>ۿ@31Ë,?sxҌ)]eqV=qqhbԔGU ;(7%L* 4b<9{E`x^e4dPkB! %.$| \x*1d !6|LH"O&e a?L B: [Rt hBa>wqn2/wt2UKOiGίl9L8}x M!<&ǖZ#cЮ2XUrg\2yGPRH#IOZX~VHڳNH>KJ:sCJSA3@%aG*ӽ_46TbW!~/UXtk{ts,s*TtwdS"wQϚX쪢!(HM]臡,N@OlJS@161vբ.Lbfyz8fX8l~ϜK±Fma*Tk,Q"vFCq.cՇ1UJZN}rJ pN̘>VWSۨ]PP#-ՌuKiD N+ʦ(A6̆=f"UPΊDXJ~"璢XW8 YfΟ4\hbMT1?S(_XRHwp5ͿXDY燕ms{/ex$iE*Mdd儂& {x٨w"-)ݝG]-I'%0DK}0McHҎ3 87DZ{ IDAT #WYw;u<ߵljv_30CIZV+2I8a #zRg}p ~z_-A.dF !rn[1cCRЅ!N9ScH- `DIix??c:4;R]/Z+Լeļ3P=K mh'JQĎĺU'u[ji`{Q6(v;q(;c`k)U8H 7B1<9h@`/;it)(jy}'б=I>9ݲSDsYA%{8s ƌ3ׇ>Ao'MZZv҄rf׳zbIuh%낸}`ihm)Zl27D~rm>OBKTu^de sHZGqGǛ媯(hƆsa)uiPJ{HuPigu-MrF?wV᰺>+;}I77b^ZㄻF۸ٸgK)G,4\kݜy_.N}evl;GoVhKsTvN5^jtf*_p&ks-eX9z;3zFG +8+(?2#{%z6Q-?pmԽ1yajidF !DI 2 BAb#P8mR76oy9 gZ/}p\58l f0y c|Q % [ O6c*2c;HD}~E珟}M1o7 (9&"d<>O/g2U ^_[Z{{ iL͈mc0Ud 3I =Z%ϗQPXvբClL'Q?uPaezW ?V{h=@^c ؖ9eFcMZ._ƌԢaqdmlsb.(Aɼs 1%2Oj0N? ̎Q\O4;*1y:*!DpDz)^:P5 yyT}y WGriieO~zӴ[>Ҿ:D?X>Tl-gRpu^Zd`GQź+;=+H+5"%I/J<Ȋowe=fb!l^&Q>^I3@A_##1?`neKugh(\d#J}x"#}vGeu1iBk[uDFRq$Z4SpuQLkgACLRM?0d'iGsOK]Mr9eSHUTldFsnx23u4עfM'\bS~ N1fPJ>sGUν̙=N`3QOݑhKhÈ)w$?YFCԬZJ3')iœe֕Ց28u(W9L 0C?`^]#QJX&z'jf;7;’zP<@5Ya;Y2kϱL QDn#st_z˞Kz;Vv,3F[H,Amd4-&e|Qiܺ׫m:o~RH!Rnc75iH')< g9Z!)@$FMTk' lC;'e^29[ZH6Lz"mAJ^~`_*BFV;(/0 VoK R\T$F&Ęf֭?pǑW,D%}J" e'ȐMbJ z=z ߮z)O<)$&aa lzeʄ6vnTiy,qO kOh^.?2tugV.BFbHwkזE@w29P-ߡѻ|JӶ*\a68-BJJ"{90,9`"u"juzT2~IIIDfzxFڙשm+~$$Z~Nw8|giiWlx.X~z1k˘=UN@8KpI囫9pY11u |Tz㜯?Ъ DΈqبpw[iacx8=3eyaQnr8 >D}5!GP]$2I.؎nnLGYij>C@$(vT[HKM%u'/s9e//!m?g?xXۨ;5Rb9ihj|4hqvJ~$\f5 ~u-cb ˺eiY>Իys}]^Kz^>:Cݙ>3nU>IZD6#jduk܈H~ oaL e zRR٢%%y9Urm@9֖>N9XsQ!8J&1Ռcd`u",|,fJד@߼m9NӞc\II$װvk+o50;o [; >> z'im=ɵC{.9 afj2 gd&d}d%"B!3}nU cd!1~ǽ+p8G('X3Vz>Z7:4蚑H* *,2\?Eq6:OvK_&q |(tOiݏ↱AX0V$-]Jё\0Yjl;@"iɴ:Yx.mAٱ+޽ UV[7P]=͖q,IzaWQ|>@t%}yp0new^~tc;]T# -̝ܕ9v)n\EtUt[ivI sU }( ;M]&w߅;x !kˑs8-I_EbmL@̀NDJM *Uv:֋2{կA#>z':4U♮/K#u؟aHMZ [bm @Fٙ9$0CS'hTSnovc띱ˡwb\ɤDtF \aDĔF%JE?e誧Vty_RŶo(A4o]=ڨ1Ү9׈eq$ w0l?0N/dmyl8/?˲|Eǽ!ad7*DQrK-wmrOm0n@yxpFV#(KSm*VFd1D8&zJM)]c;J|ui?dX=A2P 7uBFv(zf +q>!seY#p[b]2M{T_f~ IMPe gŝXFʰ?OȈ4 18ي'X$fli)|1m06 `h OSc/vx#?HuX`,Z5aZhT'؛A o'e*.ώ-bH!j ʩs k~sq~ƭ.`W;X%~61s4ŋP693M]cn:C~e/w>La0\$%"eoE请G};?p\hD4=f"w](8ڸ &@N7)_Bz)4=MGG!^MChȃc˚11cL._$b ¥sY|@2˖=*ÝwcY7+:i~[@𸕤e 4D\9ƁU /܎anh!i,TK>؊w%tXFNA/)o*xćAd w`wr69| 5|ǁ44>m -lhPw$i*f^WѨHd^ɒLf^>:iNu}[@E]Ed^[.7R1|-=> ?L39yYYB j6]?db~0?CcFt$[~ܭ>nʏ q ))݊b5D$/#UOc3(Hтw 4PzD`ӮDxmܲ5OBaG5TGOwõ9FHKHF9=H^'z3z,N+9j9L@7=3P\Y*rDy O"= ȋN2'H K8wy)6g?Q%*ojo37}|I|93Kf07(s-*YJfCJ'?wƈ58Ԋgpqm.L O|B9N:+_?8W}c xq;Id?(7vpHcev??`B$#2/Րf~܏6Jm>ڑsHL|o !/O"|NFbiǟ7n T fK oѡ)gQ4_M׋^ l 4E,88CXjM~48bJnҁ HfCS-z 6'6pl9>jz2g; |6<(V:qpK?#^%sxkV x _VD~> s濦<5n)׆ЗUQ|:\&A9\y!5SxlU /P^F1sg)EW4Ѱ_fL>8_BԡLFW1 tgi<Œ*N.sԣEE_M{ ܞD9:B8O2_ bd ¥n؇v=)V`788HOgu eQ<ґKiְdb/ !8 U#n\6؊c PpWjp;v2+)' vYb14FɆ&6= XqO)\-VًI;*lKAvb݀ 20:v| Woz-g*m4mvzz9Ʋ mf?_ Ƅ+-aa?,&,sE{6@y-ÝHfVGÍt=5={k0uxe-+f_AnC(n2ZƆGܴeCni`y$u2oÿm{9saԾZNcd!hC&r2JQ|L[ۨ&E1^MFw#3VQZe !d]? mqRG*plΫ`/{ cX[N ]φΔxipETt |Zpż[#@>h5΀w4UU/G^nre6sv:YNm=P/ѯߚXv Y-G*{^ڜGM#Hd{ۘcg.!i{8lƘvi蟰Q"ADӍ 6wnx:yeۣ\]2ȋ<QrzL&mt k6[ݴl ƿk=q#'W|ױ lNu5t^ё)$`0M U >a&L)I}<_Hh?-zt>rJIM7BӻP}iIK޴PapZ&G9g:hn9nKH;K t:ˣ91\Zrgi}΄+eԿ#Ok/Em”юR\*?V+V8AJV3)ˑ}5Xa FNX[22| l4=UXnV4V` vd`{KBzƝLGy߭X"AiZIZiwc][2Qi\^F2f ({;QX@~&l|peKvqid'Z82S]mdc7~wRYEv.IbL:׵A /fp#֧nۊ*_?v R]= ^iU6alXiHz\iaּfLNRSlҥT IDAT?,9Qlo?8;wԔ5=#a wਨ)1P}uaa~GHV"ﭢfY=hD}:iI jzNj (p)NctbRAA 3RA! k#2HJW#PbD@әM,~tUYcˉw`nˑH*tUSif,c.AM3ըllRt'a Ւ ֛k.'[`хo@BQɩ  Qiڂ  gxpn NW*IGt[q}6ʎv2\!!7qmx$dP=I-8ߒ~.hIp@qIA2E,FN_XAA##AA.=ioJ* `{_HGE*cDy[ cPi͢P=̤bvoAQD;iwٙ4L; 38~  ER2XuC6G# ."WNG8, G~Պ[)3i-fjMֆmԻAA.*) pZ){^r=8ou!'VHZto<We Wa MΎ8xB+QX42`c$1a;"Zp)؉kKR YrCgLA"kJ@1%iV,o}-_MpUNf  'BLAAdS]jhZ٘KμB[\AҦ+Q&D "v72EzeYR;pLNg[WJFK7)^Ap)AuG\4u4U(*n/t {hxэL?[qvf~N\LAAHAAK| F+%2x5y2_g;;v1MKsI`0^@:A_551=vJ$}V;>IJRɃg&  p1HAAKё7WE/NByc>YI]̫U;=x 3!Lre^?^$@2ySr].sɛ<hrxR@?GtE*(T`Fyy #g4C~xF_6EH"h|L[SAAĚ  E\.7*=vhꕑvĒGW7|-'A6ïߒp%3E2r9 :ey,^Upv՛q$UKr|#0sXB+*mQpHUAAu>H Y7?iGCK]jؿg] BAA  kȃ}[tD  3RAA}#!  =/AAAA!:#AAAAADHAAAAA>3RAAAAOAAAA!:#AAAAADHAAAAA>3RAAAAOAĩxֈZ: ?q`WϸhQVi, ?-B15%8;]4j;뾮O5%]`$KUj$tq=S;Dޏ^֏+pfl2?ߞ7OPT^86,N}hkpsL^8v+&{MݠG?ͷ1Dx$eF&Y2ώ=G{y%CHf n8;b2evk IOB}kuaRqI2),̟1ɾ_;p|-{ l=++&ێZHT\~:QKyjG)tFjvv_P?bQe9QګulYG,f{gS1b}U,X>_!BjRjrp*.}A"Vv[-w4R+0hdO:rѲ@+q?Jm뷻cuSfBr.J~8p ?cfӎ Lt&M`@BqM8nv֟7 $)ZWK;T4TyZIfּmN1ʋدl #s-X>l"5fW?H*tUSp2HH\j{#^%^N?Dy%}-ѭZAr7 Ԩ$0bS 0סuVܜOׅ;Ҝ*}UQ^&Tc)n1&u*:r0{ku`Nnl;r=d!""E+osjhXGP2/ˤ]Mlz`zg;?Vk\f k =c%O5jIbm5o梤9w"LґdEB f4*>;-&fc?/~RL`܆{ ul~k@D&23k#kQ|܈dE?e-n.nt1EHǑy {j~|/-C(fb|`z?a95c:V/oǴue/)U\:9"\X#ߙbZ q*׫H(I4Àn>WjɮW,+9>MO|H>ζCذXd :dz( hhyFyQz؏f%fv"})PdhJO#"S>D7 /T8!be#t0܏gFGEln^SNXvC'.|M#v=hGǵ$_ΑȨ(\wZ; p?^|$y p Kmqaa*Xv Cptw;LX<:цHU9ɠ"bI/%XL@;?P/M|Y NngقI iLo;&3QvtahŏMK;ƍEƿFsd%-q+@'!_Tʤ uQh#íPc uaYn"͸?MmGudFM&9z<{oqiri^VgKr&9l=}%{4|tΔ$հ8d a~oκk1ៃZf^!!W6N.لڝFK +ڎI0Z0۩ lxj'}=nU1 J ׍q mKYT7ǒF<3̀ύl#jjG,5Mo'D7܉97йkp LǍȸӣ%>+3aɕQdޘрO)I""j~`7?85| k2Mӳs{rI٨Oo1Sl}6lgdՅTx=[`=O2tvasJԉZ׮ ˮ?bb0dr.lw;㏜Qpl YJfO[h9 wڌlzIA)fh,޸vfgK'Gڻ}>]wprJW2k ."Wr 6dTf}uT;ǒ 3u%TUCM=GI4Yٰ'2 XjpKeÉRBqMKO`b6RfUxm*9w[<Q51}UC4;[(ޕHEn敕uKӽˆV:,%tvՉ%"92/BV-)s:}GC(;L(eXy[$bz{ݎIjt_L/\3JX*lK:X\VE݀i/B_a(A~K72wqJ5[(t);Qq\O1uJ" tӼ˛~o#,Ʌ-3omAUgn7d]x)LY"s{>b*\kL:iYA9@P3Q~o F wm0SΫ=$=z܏vLի$12RAd[<xk OX+Z=o.om%pȆX:;㕵+1Sq_" 4P@#VroXrOXEr0`)_b E/~H Rwkp_k8΢/66C~HR5 5VP$1bFZF ]4?_PLqKYqJn?/aPVF5hf% ׮iH!bܣ`׏mdñ\7×N./c{̋oRf`0c;S@EB2i26Izt²[$>S`}h)D sf)8Th؇ʈh6lϹ<Kwte"-1=RTָ֙l(MC {)+]u/cUԡ׬Dӷ Jkh8"#)&w  YvRZձKa~^º?ܽ /ӷYB?E(Ϭ#͡dJ%C#"'EnG}M2)D-I)/%Jv=KujY!cv${p=?D3ޓN~/N1\P\aCĚ5@%-o(HB+#s'uš-8*R yPľ}tޏRFI"q4o7> 8ˤg?Tmzu%_pbЏhb3eyl*5Pl0c?: BUe=xukLӌe#>z$IDdR>3H__NA$R>;)I{Eo(B\t<]"jDRb*[-0|a=UG׎r#x_E^Rga`kMZX-g1 j4 ߀EZ PMA i|TiV; ./2w@ʦPG St;YFоۆw(y6g|O.2~Qٳ#: AChOLCF [Ȳ;e0<\ʟWS񐆃?5bG|U,6&"ڎ 'L08ǫW\983RA5C{=d4R$M6l Wk@0ET=|ts3xEUJ$+r7E.ACvgsh )77|dݘ~H$?b;Ը:,o?I3;Ly#VIx[e SE4bh}o>(LYJ_PTG ["k\4i+Iy/(!se{f+oRRN4x<7J}i@Qw>|A`O\ jZhF]lo_IӐN#i  }(Ң< 'H'1t')~1GTcۆK& sMb{.oETӦסXeJ "G:uGﮧ7GP\#w] Oo2A*~; ~ /ǿw+Ύω25!kfӀB?o'Md1{B.辜8I?,Bn݄\M.A݇; r:xwM*)il)FxG>\y2fJ/\vCRƻ4>xс3 Yuo:mLzRlz#QNjDּlBpoG7Ý4ߔ3葽ح-\ˆh?\)R3}CW2&o*HI42i4`=阠L_TM4D{O'YsOg~^jgN.YxZtw֋m.eCaɝ42`ܳlzH\{瑧~~3Xx.mA h\g-D^#'3a4WP4J ΁WR6AO(Zېd!Kj=2jS- W4H-@7JMaw JZmΣ2 xG?8y5N\v8_!N:r4mA7J)?fN{a~΋,FnYƀ IDATΊ;LՕa8l ԇ#ug?-WNn:@B"\6 )!$`N[\H8;eC 4|%w%1T 2@>E-xoیiUVAon9o ,]T}1ټ( 8$۶h 2ϡ/B䌺.$E P')+Nj/is|6JؗC>H3Q?@_Bm383X_(Lb] rȻ9f w fk?sx?ߊ&d |OR%{L/B}2oR&HN w+ ~s^&DcS'[h-㦼EH^vLgz8|> o1;gG $]wK^oFΧBј&KJ U4G̒'j9#y$bT(I4q͌e"1kp2a=nQ#ل;@ P[Pe23;4k%<; ).bBV*2UF+'ЛK^!kL3%fk30SFl1źd^kW㛕ɱfױ`ۛK-܅l|Ay)L>e(jʷwF, Ž % Dt">֏*IH 1@WcC81󕃮)pYyi"4UώQq^DnCxϒksT idiTg )(~Dr;N!N}% ,?md fti7'̐s1*%]OoK+[ m zIF_A[dG$, ;)ٟ HS12%͹(#2ɣwr2 2i+(_K|,=ƅ##$pGCpo#2 1.Pz N I&njz&dcGZFs}ԘdT |6^/RIyfk+&357uk9R@ǘ-A6KIwEx>WăOҠ;Ak`W:ٴ#?ܽ=4C^M;,TN4LwF+hGʉ _xsX8.d$ 8*oR)MhTJZhec0P$)b2VoyL?:I`ލQm'y8xIS~ \wH'԰ {PYAIkPHߩj#|j9jx=k )S2%\3$ ̯-"ۆ7nDmȇ__T(y/.E޿g\jO1'9#%T7/@mxɶ߱3[ Go1TUqˤXe~6XrbcN7ehBm7ice ~qk-N,+dTKP#E:qڑS&6Z31ۘGV,hN5,5C;Ow#g棟>?9ȴvX>zC̫U;=x 3#2Lю{ɝ4{)6؟vɞ}ԳjLkUߏs+_*f: |ϓWcmTx.uÍ) ?2%ȯ]69PWZL'42)V6c``s^k~i'Mpoعt5Kk|pCmdc7~wF֪PCuzkKXSv=)ئ!Chby~EvY7skcH3>ܯTajDWfK daKh= - >d |^ر lNuU8nb*J!}8%xhF )' vYbfvwn { J`''7b') (.`ZKh?z+{Xk5a}}Wlx'led5 %}M=AXc4 ##x߂+1-<@yzTc|[襩5#Fb=@pv*zcobcJӪzLlWb +y߷`F{}HqJye2om6l8- 9Icˤ_3e~6ذ?j󊋜dzal{e&e#5y݋}yTCW^*j' m**vtjׂW1VqNSf hT%Z  ^E)AkR~H :۞%Oss>9|r׻y=TݞĆs0QPu,JO4S lI}ZUop+/`ڕة/POfwm履yT4pF5wp#䰩[D+"whk9O纛W/pM5J2^z47ziJ{g7B@ sN9nƷ^vn27I+)n(ǖ󻿄*iUΔ*-G1kutXٿ(L Yi@ zeTWG@Ym:] e# ƭN+%:>aE߃@ @ 3M&Vg=CdgmFĨK|G8>X.l2^.^^o&pWue8]#z;#)P]AS|> Œ18% gGN,Ыs0px@ѪUW} -Wg#  н+脃(Uirz<'x d9ᑄ8|[ .|MZxtk>G>*s)݉AFQnzʴXk}D:,4 yw\nr.Qz4`QB_amCPxyK5Zhzi!]d*puuZ+}->oCǑ<]6fV 1=Ǻn)xC^_Jy!! 1l-u\"Z$tzz=]wױ'Aѳۤw{$ŵ6HL6`qztE9v$tMW<ԏw Ohw)-zt[XK&! )ߕG(>\Gެke Z^WȊq0=[qnm݋qUhr6됛6;l7rWOB#B<ºE@n{ kz+/ SW趤NsD>̏](Hz@lEס%)o-DhA8BtjE:=-Z~{>H+l^_N5iyCGɎRJvѾaj(=DR2<[w#atl9zS&=n{(>iQq{?З1 VնxJy&gddS+Z+L[`D!yFy0@I@ntbZO9̑q鮉(X1c>H-$̛`$.qPZvjߧd9## i4:Ӌ8yOF#j=F,JYDe.B M^t &N/ćQbz޵h"a=HDqs ˘1ᇃxW2BxoXiw3 BYlS'Q1)ƕ'2#=<}r;3!D<"ZnяOQC8 +#>s1w44BvH4OO!l_4qP_Z:t)N: lmZOi%>-!e[6c^l(}Jņ|=Σ D%RLba' ;GɪMԳ][N`i$cv=efq8)<gclrpH!( ^YKV*YfzY"$\g6OUwE&/wOi8 }{73})/stO2krv"O@};2 lqW<{ 3Yv_"?G?'(`w2494W [2(@Fdī%dYQ̑6?nQ_H"f@Iv49X6Q^l\AG9B}](XSQ_ T8#cPMȞ2[~ļyzx,PƎbW^"a2p 3ط|7P^ BP3prb#I|fIwCXg" ')+ YƨXD3,e@Tò ?Dd [ױ*#q8զEddY}-_&cu;W"wdSŬ }]AIsI!e * IDAT|-͒|[z+8n! G(\AKw~ďZgqS U5df_(n#ǛL_vI%̔G.KǼi BXD½{xX0gL׽C Ʃ0[E|> tkE@Ǽ|CBw{V?Ŝ[r귫83 _$bfbVvʝIOxDD|f @#-jK .Ll*I^iz$"@M&9WJ6lEdQKxr+#Pt+>m{asr~g&F5 Tφ & 'gw嫱S]6L-\ѝ5c; 4!˵v6j+jhvkF2?8rSQ?N|W%Iqh]ϊ&3%9YlsG=T#wCBe \N.G$Z[  "?.`e&]C+:~ 41}7^RxHơ ̞،YgU\ز=fN7|cRI01:.XN+3kt*у@R fAEx%S$T:OcwoWyu"1;즺D#O0VC;+k$#Pu,y?s%E3yt:1'u$"Fa1Џ5&Cq^(&q0$n3߶RoZjmA1u8ԋ0f=@@Ӄ)Y G;pq`D"ZоnzAUqmi`OR7+nsog?gx_vc2wSyb®3#xo猼CŬlrgzQ{zydB|$*/>qxfWQcPʕyť̉ %tpd.gҕF>G2S1Yy|p a2 dwD%oهϞɢH%גMY dP on(Ngtu]Lzhvgp$Z=`$ f,?yr騁}4TcƠe1IE:x29t"uJŜ~7RpFCw'}K! '-`*&oJb C"/5hC;~?N{dG3;,X"/-O#~DAXױ9Z<n8Q̙?n;u>M?縉H|AJk{j{d5ppLfMȻNGy$j`/m=0f$F1yF=e=7qZ؞C tsfu9ߘ;ςU6 o<)TPm`1!E2@AD4ȣBr_PttߍtRv8=Ʈr+{Dhut:^T1fn! RLt9ID#~ʻ?G-qvb2< (+ݢ:/4(Q xw9fvv 7&7%4VsQ}J-oQB#OvnktH(D9aeW`~ؙ@DU\s˓ݑJHMIy{ԡ[{0yﬧ ڈ?gZ\JF6YF UWo2 N }VVdJ$t"9Yiu7=SI7&=zm)%:=72~Wgaaj趸tV9,t*xCkKA(2{ ݻݘcgfj+FD!UG NJ54x E5t=+C-$}BD>K7ۛib.g2iCpsb|w88(R )T6P1.,k[rEƀ/prwٟ-6U&+~-"n-31+Xq^ 1 HX;42ukb,W)>vajG) u..h{*LzGG5lI"b~^|E7PCQ$XBWHژNOQ҅Q㈲P^qݓnC*GO Rȧ\) <<~S{Gd3@ b& CowZ` ]`62ђ&/e%32?Ir撛P"%Kgti@Qw_2g8/_iYˬ[a᫕8OGλ~細cJF6:\\W~La±*b/ D>5cbO/<|=FDFfIZ7R)K1@juCq1nPOp`.bAgoKU!vPY0Ofer^'m&aK^&}q<ѣv9yM;}Uk ?0da$a[8cpϹe}h=ʇGVH"9cq|[iRN,qYtA ”7KB-mϘ6^2dBU,_:-&55*Cv[+v11x0&`!saiNs_<&F?!3 4nϬ6i"2ÎvY*iYJQ/;5e؃[D89 | 9s| K9"c\aL,^̚?ԢJԄqkVEҭO-Uތj܌ѽcR, ٜNR`PNTl2S%!LG3CjwEuH{q٣r9/9b+ҭ)ӖrFk%n?:1bt?O%55i{ ǴIf9 gQJך*r~Fs9N#mmo֨87 7;|,/߿6+(a$bXyϴ-Kzz1X?ZQWiٟAI]ErlHz] ]]';Sm;AIL qI.L*# w]_pב{3Q5C.6 ZtN]ws~ʈIۀn#-(ﲡ/\ӱټR3]:',Ry8e 3z:x+2y~'u-TԎdd 3%SVьIKK#MjwoQ퐈I$ɡCdկS|Ќ#Mq`2TL A8ZˁOd]km)$f*jQ>M\L3U{?.!YɤZKԂa']Q3Yij?4!0 EDT(NjplE9#AH#Ƒ<7$1XO!hPn 5BBuB <6l??ۉ[sMū6 E%8Sw[woc|bԎ{ L`oJNaCb0)f$jOPQrVfK$oy> za*)<$3lKRy2C|Kg%.b 22\6۱WK=PAX|<ÏVr?G(1g:2pʑOdL|d Y(w P"u#Jp_HKXM(.UIhwB@C9 |abmvwsj T2Y5kLjM\qRҟ|VO^%7 VF#!ovK}"&Fo@A'{pX-x>hsn>K'Rgka eΙo#w^ܯZ9r\Ԗm1Y2ɷw+8wsXs=\rjL^恓L |GW_'uwP^UB{ #| z>q`;y S.Vzؙ4Wt;rlFF0̧X?|1 &ݟ7K@mDGr;qp ;N8ꚱ7ՠC.nS@8[)G J6h,T$'~=݁}a=>ɐvla ck+ȇuy!M| Cf+ 6lͬ~nQOmoa&>9a휿ul~{ʺ}Qv)XLPo=O ~O?W6Hfpӟݾ>ɤEPL6Yüx [tZ=8c]C ,?ëZ׺tBH: !w׉ -ѤN(N kkɌfΞs+CGv΁x+n؉+ *v[yxJOa=mǴh$zB'Q6Z?̰a2"%;$Վ;[ywEO~h pjǾ}س?|adf=#pS {ݮҡ _/ jbrCD&OcXpHǦeLQ8QBa8 F2r w!1HILi7h 8m ka*- P#?szs/;8q sKϸhZN047|9WEz˿,)7׻F`-c gDnÉ/l:tZ*eeS8Zh #eD?MޣiijCU%$S 'pRJqp{>Ld\kn dE$|ZRW2&uGZVU1 O|QKKdz_eD%/ y"GZ@uE4Q3YB[,H'jtm!GE+>׺Fʋ7x4*8۝\VRecSn89n>h9{>*쁅,@VIl^e/ (JN&C1oI.I_8q~H"*Y)-.aKښm\d?a8s㠳L|X]tc>69pBrutMl<ੇ{7wQQ$|` _cV-ig~"|˔$GU݉CqiS_coD"ºi@ ?@0?cC%Fi;q+Îi6geZNQpmzTU4y8 rqv9I$LP%g<6pۈhFF2A{$O,=%*M ;]^IM8­]8jTM#n\>eb_k&5~2q8Z2Ƚaw.[YMj`+<1u*3䞓\Dtn,RqHu}F̴65'~طw{VCmT@ vHc Hx$x{\`ul40mxiӈY+AfleāHqm-+2[pqwSbqvGGnD+%yz4v8^+Qr˿̦_y=_uZdKu)YLia_dLi>v^x)^YA6eV[Iu{E%n>]ʪ_Rգ{2@ dt#^h6چd^|4{-lOwb4'5 9^bVےyRM*_RVQ9:O5m~9ٿ6p7Qݽ痰-"˜l(gr/mq2 Whӻ:gݚA-ef~8ʯZٱV3KwgذUk|\Ϲ%/Yzc` ] |&^r BF--!.)4 ':=< UyO<6h!vM2:i^ӏW(]Dm Hr ݁O&&)0窳ObK֭g iD,Sz]Z"d|2*j]?|myroĆhݢBb|SkD`pv葒i߳ POS!)#uif8bP|1):VF`>]mػr&]Wt129{;Ͼ}6c`CSĝv tzz.'n2^d|YB~W8[}x~ipֲǧUJs]+d5צKrʑ`k88p㾑6>=%փ25ؖamMps:.vF2vyڛcf6?^zN" F{q dD m;T%2P2=f5K~IJq'ᔿImAG-$g%Pr9Qs{ɂr\U\@Z| ZyˍJA5D{IDATF;9qD'&L6#+nt|6ԇ7_+i-M04T&ő6?S;]v]9$t#;i2 ҳ8 8PVps>]M RSc1Έ$ wѽYM'P|QsF0fGgsͧv Cf?X$yx[l|+^BS+H{Nl}[=7p =frT+Ҙ'g]ڽIٽgovۆc.>d%DF6}P̦sUYQGɆ#룍[u;&ḎwYJgYTVapGLz =q=YͤqzrEa~7LgY =k٫S&7 ˝Wkô~>:{ K+8.n[nm6Tx ZP륔t:,l^OXq.^|=onFr-I.հbp kSH(P{O^vzYdҰ~y_\ 0rG 쳄"CgX%-ʮ9{`Ð_b"G|M*ԩ ,Ϻh*BwU{Ud>@z)]"O6or~.04ݷc^I<,q3UX?ѰamB*$DC'>Ƒ)H;c08yKR1-r޽˞K6tO6|GL+ ='Esւsݖe]j\M%{O4$(ʨr_Cjq9h>VHYFtֲyy=BԥM %Һ -pq3Jtƒn[0rX;2SLM2œjf_)AnaFf9E+GeEZw2E$UaViInKޫkPJHGaEқըc{ ˢdc>)\i;Ù/AS0Av VR`7} I$ݍ!J(Jm}J Pu,JO4S lƍ7iMoK+F}Iz*vxK| {nLU7+e0y,α8uп@pCP[_'KDP!-6Yp0HAȭ6{""q%;7,6?|LMӦ7׃Ldc<(f!_QF^WAInfgJƯh (J`L~o늓CܴH@8{7-B7@pNd_#I8"1\9"7#7Qn$]B&%7D١o"ǝ'k|Ţ?ȓU/c -pF @ ?:wQ@/" 6|w=ȏ+1H2(ԇh.79Q]yF~i -@  )@ @ 3R @ @ |%۴)"M[ @ @ ӹ36@ @ o"M[ @ @ |%*t*IENDB`sunweg-3.1.0/requirements.txt000066400000000000000000000000311467730223400163110ustar00rootroot00000000000000python-dateutil requests sunweg-3.1.0/setup.py000066400000000000000000000014541467730223400145510ustar00rootroot00000000000000#!/bin/python """setup sunweg.""" import setuptools with open("README.md", "r") as fh: long_description = fh.read() requires = [ "python-dateutil", "requests", ] setuptools.setup( name="sunweg", version="3.1.0", author="rokam", author_email="lucas@mindello.com.br", description="A library to retrieve data from sunweg.net", license="MIT", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/rokam/sunweg", install_requires=requires, packages=setuptools.find_packages(exclude=["tests", "tests.*"]), python_requires=">=3.10", classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], ) sunweg-3.1.0/sunweg/000077500000000000000000000000001467730223400143435ustar00rootroot00000000000000sunweg-3.1.0/sunweg/__init__.py000066400000000000000000000000321467730223400164470ustar00rootroot00000000000000"""Sunweg API library.""" sunweg-3.1.0/sunweg/api.py000066400000000000000000000361331467730223400154740ustar00rootroot00000000000000"""API Helper.""" import json from dateutil import parser from typing import Any from requests import Response, session from .const import ( SUNWEG_INVERTER_DETAIL_PATH, SUNWEG_LOGIN_PATH, SUNWEG_MONTH_STATS_PATH, SUNWEG_PLANT_DETAIL_PATH, SUNWEG_PLANT_LIST_PATH, SUNWEG_URL, ) from .device import MPPT, Inverter, Phase, String from .plant import Plant from .util import ProductionStats, Status class SunWegApiError(RuntimeError): """API Error.""" pass class LoginError(SunWegApiError): """Login Error.""" pass def convert_situation_status(situation: int) -> Status: """ Convert situation to status. :param situation: situation :type situation: int :return: equivalent status :rtype: Status """ if situation == 0: return Status.ERROR if situation == 1: return Status.OK return Status.WARN def separate_value_metric( value_with_metric: str | None, default_metric: str = "", metric_before: bool = False ) -> tuple[float, str]: """ Separate the value from the metric. :param value_with_metric: value with metric separated by space :type value_with_metric: str | None :param default_metric: metric that should be returned if `value_with_metric` is None :type default_metric: str :param metric_before: true when metric appears before the value :type metric_before: bool :return: tuple with value and metric :rtype: tuple[float, str] """ if value_with_metric is None or len(value_with_metric) == 0: return (0.0, default_metric) split = value_with_metric.split(" ") if metric_before: return ( float(split[0].replace(",", ".")) if len(split) < 2 else float(split[1].replace(",", ".")), default_metric if len(split) < 2 else split[0], ) return ( float(split[0].replace(",", ".")), default_metric if len(split) < 2 else split[1], ) class APIHelper: """Class to call sunweg.net api.""" SERVER_URI = SUNWEG_URL def __init__( self, username: str | None = None, password: str | None = None, token: str | None = None, ) -> None: """ Initialize APIHelper for SunWEG platform. :param username: username for authentication :param password: password for authentication :param token: token for authentication :type username: str :type password: str :type token: str """ self._token = token self._username = username self._password = password self.session = session() def set_token(self, token: str) -> None: """ Set token. :param token: token for authentication :type token: str """ self._token = token def _set_username(self, username: str) -> None: """ Set username. :param username: username for authentication :type username: str """ self._username = username username = property(None, _set_username) def _set_password(self, password: str) -> None: """ Set password. :param password: password for authentication :type password: str """ self._password = password password = property(None, _set_password) def authenticate(self) -> bool: """ Authenticate with provided username and password. :return: True on authentication success :rtype: bool """ if self._username is None or self._password is None: return False user_data = json.dumps( {"usuario": self._username, "senha": self._password, "rememberMe": True}, default=lambda o: o.__dict__, ) result = self._post(SUNWEG_LOGIN_PATH, user_data, False) if not result["success"]: return False self._token = result["token"] return result["success"] def _headers(self): """Retrieve headers with authentication token.""" if self._token is None: return {"Content-Type": "application/json"} return {"Content-Type": "application/json", "X-Auth-Token-Update": self._token} def listPlants(self, retry=True) -> list[Plant]: """ Retrieve the list of plants with incomplete inverter information. You may want to call `complete_inverter()` to complete the Inverter information. :param retry: reauthenticate if token expired and retry :type retry: bool :return: list of Plant :rtype: list[Plant] """ try: result = self._get(SUNWEG_PLANT_LIST_PATH) ret_list = [] plantlist = ( result["nao_comissionadas"] + result["conectadas"] + result["falhas"] + result["alertas"] + result["atendimento"] ) for plant in plantlist: if (plant := self.plant(plant["id"])) is not None: ret_list.append(plant) return ret_list except LoginError: if retry: self.authenticate() return self.listPlants(False) return [] def plant(self, id: int, retry=True) -> Plant | None: """ Retrieve plant detail by plant id. :param id: plant id :type id: int :param retry: reauthenticate if token expired and retry :type retry: bool :return: Plant or None if `id` not found. :rtype: Plant | None """ try: result = self._get(SUNWEG_PLANT_DETAIL_PATH + str(id)) (today_energy, today_energy_metric) = separate_value_metric( result["energiadia"], "kWh" ) total_power = separate_value_metric(result["AcumuladoPotencia"])[0] saving = separate_value_metric(result["economia"], metric_before=True)[0] plant = Plant( id=id, name=result["usinas"]["nome"], total_power=total_power, kwh_per_kwp=float(0), performance_rate=float(0), saving=saving, today_energy=today_energy, today_energy_metric=today_energy_metric, total_energy=float(result["energiaacumuladanumber"]), total_carbon_saving=result["reduz_carbono_total_number"], last_update=parser.parse(result["ultimaAtualizacao"]) if result["ultimaAtualizacao"] is not None else None, ) plant.inverters.extend( [ Inverter( id=inv["id"], name=inv["nome"], sn=inv["esn"], status=Status(int(inv["situacao"])), temperature=inv["temperatura"], ) for inv in result["usinas"]["inversores"] ] ) return plant except LoginError: if retry: self.authenticate() return self.plant(id, False) return None def inverter(self, id: int, retry=True) -> Inverter | None: """ Retrieve inverter detail by inverter id. :param id: inverter id :type id: int :param retry: reauthenticate if token expired and retry :type retry: bool :return: Inverter or None if `id` not found. :rtype: Inverter | None """ try: result = self._get(SUNWEG_INVERTER_DETAIL_PATH + str(id)) (total_energy, total_energy_metric) = separate_value_metric( result["energiaacumulada"], "kWh" ) (today_energy, today_energy_metric) = separate_value_metric( result["energiadodia"], "kWh" ) (power, power_metric) = separate_value_metric(result["potenciaativa"], "kW") inverter = Inverter( id=id, name=result["inversor"]["nome"], sn=result["inversor"]["esn"], total_energy=total_energy, total_energy_metric=total_energy_metric, today_energy=today_energy, today_energy_metric=today_energy_metric, power_factor=float(result["fatorpotencia"].replace(",", ".")), frequency=float(result["frequencia"].replace(",", ".")), power=power, power_metric=power_metric, status=Status(int(result["statusInversor"])), temperature=result["temperatura"], ) self._populate_MPPT(result=result, inverter=inverter) return inverter except LoginError: if retry: self.authenticate() return self.inverter(id, False) return None def complete_inverter(self, inverter: Inverter, retry=True) -> None: """ Complete inverter data. :param inverter: inverter object to be completed with information :type inverter: Inverter :param retry: reauthenticate if token expired and retry :type retry: bool """ try: result = self._get(SUNWEG_INVERTER_DETAIL_PATH + str(inverter.id)) ( inverter.total_energy, inverter.total_energy_metric, ) = separate_value_metric(result["energiaacumulada"], "kWh") ( inverter.today_energy, inverter.today_energy_metric, ) = separate_value_metric(result["energiadodia"], "kWh") (inverter.power, inverter.power_metric) = separate_value_metric( result["potenciaativa"], "kW" ) inverter.power_factor = float(result["fatorpotencia"].replace(",", ".")) inverter.frequency = float(result["frequencia"].replace(",", ".")) self._populate_MPPT(result=result, inverter=inverter) except LoginError: if retry: self.authenticate() self.complete_inverter(inverter, False) def month_stats_production( self, year: int, month: int, plant: Plant, inverter: Inverter | None = None, retry: bool = True, ) -> list[ProductionStats]: """ Retrieve month energy production statistics. :param year: statistics year :type year: int :param month: statistics month :type month: int :param plant: statistics plant :type plant: Plant :param inverter: statistics inverter, None for every inverter :type inverter: Inverter | None :param retry: reauthenticate if token expired and retry :type retry: bool :return: list of daily energy production statistics :rtype: list[ProductionStats] """ return self.month_stats_production_by_id( year, month, plant.id, inverter.id if inverter is not None else None, retry ) def month_stats_production_by_id( self, year: int, month: int, plant_id: int, inverter_id: int | None = None, retry: bool = True, ) -> list[ProductionStats]: """ Retrieve month energy production statistics. :param year: statistics year :type year: int :param month: statistics month :type month: int :param plant_id: id of statistics plant :type plant_id: int :param inverter_id: id of statistics inverter, None for every inverter :type inverter_id: int | None :param retry: reauthenticate if token expired and retry :type retry: bool :return: list of daily energy production statistics :rtype: list[ProductionStats] """ inverter_str: str = str(inverter_id) if inverter_id is not None else "" try: result = self._get( SUNWEG_MONTH_STATS_PATH + f"idusina={plant_id}&idinversor={inverter_str}&date={format(month,'02')}/{year}" ) return [ ProductionStats( parser.parse(item["tempoatual"]).date(), float(item["energiapordia"]), float(item["prognostico"]), ) for item in result["graficomes"] ] except LoginError: if retry: self.authenticate() return self.month_stats_production_by_id( year, month, plant_id, inverter_id, False ) return [] def _populate_MPPT(self, result: dict, inverter: Inverter) -> None: """Populate MPPT information inside a inverter.""" for str_mppt in result["stringmppt"]: mppt = MPPT(str_mppt["nomemppt"]) for str_string in str_mppt["strings"]: string = String( str_string["nome"], float(result["inversor"]["leitura"][str_string["variaveltensao"]]), float( result["inversor"]["leitura"][str_string["variavelcorrente"]] ), convert_situation_status(int(str_string["situacao"])), ) mppt.strings.append(string) inverter.mppts.append(mppt) for phase_name in result["correnteCA"].keys(): if str(phase_name).endswith("status"): continue inverter.phases.append( Phase( phase_name, float(result["tensaoca"][phase_name].replace(",", ".")), float(result["correnteCA"][phase_name].replace(",", ".")), Status(result["tensaoca"][phase_name + "status"]), Status(result["correnteCA"][phase_name + "status"]), ) ) def _get(self, path: str, launch_exception_on_error: bool = True) -> dict: """Do a get request returning a treated response.""" res = self.session.get(self.SERVER_URI + path, headers=self._headers()) result = self._treat_response(res, launch_exception_on_error) return result def _post( self, path: str, data: Any | None, launch_exception_on_error: bool = True ) -> dict: """Do a post request returning a treated response.""" res = self.session.post( self.SERVER_URI + path, data=data, headers=self._headers() ) result = self._treat_response(res, launch_exception_on_error) return result def _treat_response( self, response: Response, launch_exception_on_error: bool = True ) -> dict: """Treat the response from requests.""" if response.status_code == 401: raise LoginError("Request failed: %s" % response) if response.status_code != 200: raise SunWegApiError("Request failed: %s" % response) result = response.json() if launch_exception_on_error and not result["success"]: raise SunWegApiError(result["message"]) return result sunweg-3.1.0/sunweg/const.py000066400000000000000000000012611467730223400160430ustar00rootroot00000000000000"""Sunweg API constants.""" SUNWEG_URL = "https://api.sunweg.net/v2/" """SunWEG API URL""" SUNWEG_LOGIN_PATH = "login/autenticacao" """SunWEG API login path""" SUNWEG_PLANT_LIST_PATH = ( "getpaineloperacao?procurar=&integrador=" + "&franqueado=&manutencao=&portal=&alarme=&" + "planos=%5B0,1,2,3,4%5D&status=%5B1,2,3,4,5%5D&" + "limite=100&situacao=&paginaAtual=1" ) """SunWEG API list plants path""" SUNWEG_PLANT_DETAIL_PATH = "viewresumov2?agrupado=false&id=" """SunWEG API plant details path""" SUNWEG_INVERTER_DETAIL_PATH = "inversores/view?id=" """SunWEG API inverter details path""" SUNWEG_MONTH_STATS_PATH = "usinas/graficomes?" """SunWEG API month history path""" sunweg-3.1.0/sunweg/device.py000066400000000000000000000274201467730223400161610ustar00rootroot00000000000000"""Sunweg API devices.""" from .util import Status class Phase: """Phase details.""" def __init__( self, name: str, voltage: float, amperage: float, status_voltage: Status, status_amperage: Status, ) -> None: """ Initialize Phase. :param name: phase name :type name: str :param voltage: phase AC voltage in V :type voltage: float :param amperage: phase AC amperage in A :type amperage: float :param status_voltage: phase AC voltage status :type status_voltage: Status :param status_amperage: phase AC amperage status :type status_amperage: Status """ self._name = name self._voltage = voltage self._amperage = amperage self._status_voltage = status_voltage self._status_amperage = status_amperage @property def name(self) -> str: """ Get phase name. :return: phase name :rtype: str """ return self._name @property def voltage(self) -> float: """ Get phase AC voltage in V. :return: phase AC voltage in V :rtype: float """ return self._voltage @property def amperage(self) -> float: """ Get phase AC amperage in A. :return: phase AC amperage in A :rtype: float """ return self._amperage @property def status_voltage(self) -> Status: """ Get phase AC voltage status. :return: phase AC voltage status :rtype: Status """ return self._status_voltage @property def status_amperage(self) -> Status: """ Get phase AC amperage status. :return: phase AC amperage status :rtype: Status """ return self._status_amperage def __str__(self) -> str: """Cast Phase to str.""" return str(self.__class__) + ": " + str(self.__dict__) class String: """String details.""" def __init__( self, name: str, voltage: float, amperage: float, status: Status ) -> None: """ Initialize String. :param name: string name :type name: str :param voltage: string DC voltage in V :type voltage: float :param amperage: string DC amperage in A :type amperage: float :param status: string status :type status: Status """ self._name = name self._voltage = voltage self._amperage = amperage self._status = status @property def name(self) -> str: """ Get string name. :return: string name :rtype: str """ return self._name @property def voltage(self) -> float: """ Get string DC voltage in V. :return: string DC voltage in V :rtype: float """ return self._voltage @property def amperage(self) -> float: """ Get string DC amperage in A. :return: string DC amperage in A :rtype: float """ return self._amperage @property def status(self) -> Status: """ Get string status. :return: string status :rtype: Status """ return self._status def __str__(self) -> str: """Cast String to str.""" return str(self.__class__) + ": " + str(self.__dict__) class MPPT: """MPPT details.""" def __init__(self, name: str) -> None: """ Initialize MPPT. :param name: MPPT name :type name: srt """ self._name = name self._strings: list[String] = [] @property def name(self) -> str: """ Get MPPT name. :return: MPPT name :rtype: str """ return self._name @property def strings(self) -> list[String]: """ Get list of MPPT's String. :return: list of Strings :rtype: list[String] """ return self._strings def __str__(self) -> str: """Cast MPPT to str.""" return str(self.__class__) + ": " + str(self.__dict__) class Inverter: """Inverter device.""" def __init__( self, id: int, name: str, sn: str, status: Status, temperature: int, total_energy: float = 0, total_energy_metric: str = "", today_energy: float = 0, today_energy_metric: str = "", power_factor: float = 0, frequency: float = 0, power: float = 0, power_metric: str = "", ) -> None: """ Initialize Inverter. :param id: inverter id :type id: int :param name: inverter name :type name: str :param sn: inverter serial number :type sn: str :param status: inverter status :type status: Status :param temperature: inverter temperature :type temperature: int :param total_energy: total generated energy :type total_energy: float :param total_energy_metric: total generated energy metric :type total_energy_metric: str :param today_energy: total generated energy today :type today_energy: float :param today_energy_metric: total generated energy today metric :type today_energy_metric: str :param power_factor: inverter power factor :type power_factor: float :param frequency: inverter output frequency in Hz :type frequency: float :param power: inverter output power :type power: float :param power: inverter output power metric :type power: str """ self._id = id self._name = name self._sn = sn self._total_energy = total_energy self._total_energy_metric = total_energy_metric self._today_energy = today_energy self._today_energy_metric = today_energy_metric self._power_factor = power_factor self._frequency = frequency self._power = power self._power_metric = power_metric self._status = status self._temperature = temperature self._phases: list[Phase] = [] self._mppts: list[MPPT] = [] @property def id(self) -> int: """ Get inverter id. :return: inverter id :rtype: int """ return self._id @property def name(self) -> str: """ Get inverter name. :return: inverter name :rtype: str """ return self._name @property def sn(self) -> str: """ Get inverter serial number. :return: inverter serial number :rtype: str """ return self._sn @property def status(self) -> Status: """ Get inverter status. :return: inverter status :rtype: Status """ return self._status @property def temperature(self) -> int: """ Get inverter temperature. :return: inverter temperature :rtype: int """ return self._temperature @property def today_energy(self) -> float: """ Get inverter today generated energy. :return: inverter today generated energy :rtype: float """ return self._today_energy @today_energy.setter def today_energy(self, value: float) -> None: """ Set inverter today generated energy. :param value: inverter today generated energy :type value: float """ self._today_energy = value @property def today_energy_metric(self) -> str: """ Get inverter today generated energy metric. :return: inverter today generated energy metric :rtype: str """ return self._today_energy_metric @today_energy_metric.setter def today_energy_metric(self, value: str) -> None: """ Set inverter today generated energy metric. :param value: inverter today generated energy metric :type value: str """ self._today_energy_metric = value @property def total_energy(self) -> float: """ Get inverter total generated energy. :return: inverter total generated energy :rtype: float """ return self._total_energy @total_energy.setter def total_energy(self, value: float) -> None: """ Set inverter total generated energy. :param value: inverter total generated energy :type value: float """ self._total_energy = value @property def total_energy_metric(self) -> str: """ Get inverter total generated energy metric. :return: inverter total generated energy metric :rtype: str """ return self._total_energy_metric @total_energy_metric.setter def total_energy_metric(self, value: str) -> None: """ Set inverter total generated energy metric. :param value: inverter total generated energy metric :type value: str """ self._total_energy_metric = value @property def power_factor(self) -> float: """ Get inverter power factor. :return: inverter power factor :rtype: float """ return self._power_factor @power_factor.setter def power_factor(self, value: float) -> None: """ Set inverter power factor. :param value: inverter power factor :type value: float """ self._power_factor = value @property def frequency(self) -> float: """ Get inverter frequency in Hz. :return: inverter frequency in HZ :rtype: float """ return self._frequency @frequency.setter def frequency(self, value: float) -> None: """ Set inverter frequency in Hz. :param value: inverter frequency in Hz :type value: float """ self._frequency = value @property def power(self) -> float: """ Get inverter output power. :return: inverter output power :rtype: float """ return self._power @power.setter def power(self, value: float) -> None: """ Set inverter output power. :param value: inverter output power :type value: float """ self._power = value @property def power_metric(self) -> str: """ Get inverter output power metric. :return: inverter output power metric :rtype: str """ return self._power_metric @power_metric.setter def power_metric(self, value: str) -> None: """ Set inverter output power metric. :param value: inverter output power metric :type value: float """ self._power_metric = value @property def is_complete(self) -> bool: """ Is inverter data complete. :return: True when inverter data is complete :rtype: bool """ return ( self._today_energy != 0 or self._total_energy != 0 or self._power_factor != 0 or self._frequency != 0 or self._power != 0 ) @property def phases(self) -> list[Phase]: """ Get list of inverter's phases. :return: list of phases :rtype: list[Phase] """ return self._phases @property def mppts(self) -> list[MPPT]: """ Get list of inverter's MPPTs. :return: list of MPPTs :rtype: list[MPPT] """ return self._mppts def __str__(self) -> str: """Cast Inverter to str.""" return str(self.__class__) + ": " + str(self.__dict__) sunweg-3.1.0/sunweg/plant.py000066400000000000000000000116421467730223400160370ustar00rootroot00000000000000"""Sunweg API plant.""" from datetime import datetime import warnings from .device import Inverter class Plant: """Plant details.""" def __init__( self, id: int, name: str, total_power: float, kwh_per_kwp: float, performance_rate: float, saving: float, today_energy: float, today_energy_metric: str, total_energy: float, total_carbon_saving: float, last_update: datetime | None, ) -> None: """ Initialize Plant. :param id: plant id :type id: int :param name: plant name :type name: str :param total_power: plant total power :type total_power: float :param kwh_per_kwp: plant kWh/kWp :type kwh_per_kwp: float :param performance_rate: plant performance rate :type performance_rate: float :param saving: total saving in R$ :type saving: float :param today_energy: today generated energy :type today_energy: float :param today_energy_metric: today generated energy metric :type today_energy_metric: str :param total_energy: total generated energy in kWh :type total_energy: float :param total_carbon_saving: total of CO2 saved :type total_carbon_saving: float :param last_update: when the data was updated :type last_update: datetime | None """ self._id = id self._name = name self._total_power = total_power self._kwh_per_kwp = kwh_per_kwp self._performance_rate = performance_rate self._saving = saving self._today_energy = today_energy self._today_energy_metric = today_energy_metric self._total_energy = total_energy self._total_carbon_saving = total_carbon_saving self._last_update = last_update self._inverters: list[Inverter] = [] @property def id(self) -> int: """ Get plant id. :return: plant id :rtype: int """ return self._id @property def name(self) -> str: """ Get plant name. :return: plant name :rtype: str """ return self._name @property def total_power(self) -> float: """ Get plant total power. :return: plant total power :rtype: float """ return self._total_power @property def kwh_per_kwp(self) -> float: """ Deprecated as API v2 doesn't return it anymore. Get plant kWh/kWp. :return: plant kWh/kWp :rtype: float """ warnings.warn( "The 'kwh_per_kwp' property is deprecated and will return 0.", DeprecationWarning, stacklevel=2, ) return self._kwh_per_kwp @property def performance_rate(self) -> float: """ Deprecated as API v2 doesn't return it anymore. Get plant performance rate. :return: plant performance rate :rtype: float """ warnings.warn( "The 'performance_rate' property is deprecated and will return 0.", DeprecationWarning, stacklevel=2, ) return self._performance_rate @property def saving(self) -> float: """ Get plant saving in R$. :return: plant saving in R$ :rtype: float """ return self._saving @property def today_energy(self) -> float: """ Get plant today generated energy. :return: plant today generated energy :rtype: float """ return self._today_energy @property def today_energy_metric(self) -> str: """ Get plant today generated energy metric. :return: plant today generated energy metric :rtype: str """ return self._today_energy_metric @property def total_energy(self) -> float: """ Get plant total generated energy in kWh. :return: plant total generated energy in kWh :rtype: float """ return self._total_energy @property def total_carbon_saving(self) -> float: """ Get plant total of CO2 saved. :return: plant total of CO2 saved :rtype: float """ return self._total_carbon_saving @property def last_update(self) -> datetime | None: """ Get when the plant data was updated. :return: when the plant data was updated :rtype: datetime | None """ return self._last_update @property def inverters(self) -> list[Inverter]: """ Get list of plant's inverters. :return: list of inverters :rtype: list[Inverter] """ return self._inverters def __str__(self) -> str: """Cast Plant to str.""" return str(self.__class__) + ": " + str(self.__dict__) sunweg-3.1.0/sunweg/util.py000066400000000000000000000022271467730223400156750ustar00rootroot00000000000000"""Sunweg API util.""" from datetime import date from enum import Enum class Status(Enum): """Status enum.""" OK = 0 WARN = 2 ERROR = 1 class ProductionStats: """Energy production statistics""" def __init__(self, date: date, production: float, prognostic: float) -> None: """ Initialize energy production statistics. :param date: statistics date :type date: date :param production: statistics production in kWh :type production: float :param prognostic: statistics expected production in kWh """ self._date = date self._production = production self._prognostic = prognostic @property def date(self) -> date: """Get date.""" return self._date @property def production(self) -> float: """Get energy production in kWh.""" return self._production @property def prognostic(self) -> float: """Get expected energy production in kWh.""" return self._prognostic def __str__(self) -> str: """Cast Phase to str.""" return str(self.__class__) + ": " + str(self.__dict__) sunweg-3.1.0/tests/000077500000000000000000000000001467730223400141755ustar00rootroot00000000000000sunweg-3.1.0/tests/__init__.py000066400000000000000000000000231467730223400163010ustar00rootroot00000000000000"""Test sunweg.""" sunweg-3.1.0/tests/bandit.yaml000066400000000000000000000003401467730223400163170ustar00rootroot00000000000000# https://bandit.readthedocs.io/en/latest/config.html tests: - B103 - B108 - B306 - B307 - B313 - B314 - B315 - B316 - B317 - B318 - B319 - B320 - B325 - B601 - B602 - B604 - B608 - B609 sunweg-3.1.0/tests/common.py000066400000000000000000000017551467730223400160470ustar00rootroot00000000000000"""Test sunweg common.""" from datetime import datetime from sunweg.device import Inverter, MPPT, Phase, String from sunweg.plant import Plant from sunweg.util import Status PLANT_MOCK = Plant( id=1, name="Plant", total_power=29.2, kwh_per_kwp=0.1, performance_rate=0, saving=0, today_energy=123.1, today_energy_metric="kWh", total_energy=321.1, total_carbon_saving=12.1, last_update=datetime.now(), ) INVERTER_MOCK = Inverter( id=1, name="Inverter", sn="1234ABC", total_energy=321.1, total_energy_metric="kWh", today_energy=123.1, today_energy_metric="kWh", power_factor=0.2, frequency=60, power=29, power_metric="kW", status=Status.OK, temperature=70, ) MPPT_MOCK = MPPT("MPPT") STRING_MOCK = String(name="String", voltage=523.1, amperage=12.1, status=Status.OK) PHASE_MOCK = Phase( name="Phase", voltage=230.1, amperage=3.1, status_voltage=Status.OK, status_amperage=Status.OK, ) sunweg-3.1.0/tests/responses/000077500000000000000000000000001467730223400162165ustar00rootroot00000000000000sunweg-3.1.0/tests/responses/auth_fail_response.json000066400000000000000000000000641467730223400227630ustar00rootroot00000000000000{ "success": false, "message": "Error message" }sunweg-3.1.0/tests/responses/auth_success_response.json000066400000000000000000000006011467730223400235150ustar00rootroot00000000000000{"success":true,"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NzcyNjQzODEsImlzcyI6Imh0dHA6XC9cL2llcy5nb3YiLCJleHAiOjE2Nzc4NjkxODEsImhvc3QiOiIxOTEuMzMuMTg1LjEyOSIsImRhdGEiOnsidXN1YXJpb2lkIjo2NTQzLCJpZHBlcmZpbCI6NiwiaWRyZWxhY2lvbmFkbyI6MTAxOCwiaWRmcmFucXVlYWRvIjpudWxsLCJhbGVydGFzIjp0cnVlLCJjb250cm9sZSI6dHJ1ZSwiZnJlZSI6ZmFsc2V9fQ.907NnIUaMhVl2HVzAuzPTvVdD-huHNg932Eu2zj7-0"}sunweg-3.1.0/tests/responses/error_401_response.txt000066400000000000000000000000141467730223400224050ustar00rootroot00000000000000Unauthorizedsunweg-3.1.0/tests/responses/error_500_response.txt000066400000000000000000000000251467730223400224070ustar00rootroot00000000000000Internal server errorsunweg-3.1.0/tests/responses/inverter_success_response.json000066400000000000000000000032411467730223400244150ustar00rootroot00000000000000{ "success": true, "temperatura": 80, "temperaturaStatus": 0, "fatorpotencia": "0,00", "stringmppt": [ { "nomemppt": "MPPT 01", "strings": [ { "nome": "ST 01", "variaveltensao": "Upv1", "variavelcorrente": "Ipv1", "situacao": 1 }, { "nome": "ST 02", "variaveltensao": "Upv2", "variavelcorrente": "Ipv2", "situacao": 1 } ] }, { "nomemppt": "MPPT 02", "strings": [ { "nome": "ST 03", "variaveltensao": "Upv3", "variavelcorrente": "Ipv3", "situacao": 1 }, { "nome": "ST 04", "variaveltensao": "Upv4", "variavelcorrente": "Ipv4", "situacao": 1 } ] } ], "inversor": { "id": 21255, "nome": "Inverter Name", "descricao": "Inverter Description", "esn": "1234ABC", "situacao": 1, "tensaoca": 242, "temperatura": 80, "leitura": { "Upv1": 429.6000000000000227373675443232059478759765625, "Upv2": 415, "Upv3": 418, "Upv4": 525.13, "Ipv1": 3, "Ipv2": 2.1, "Ipv3": 4.2, "Ipv4": 5 } }, "correnteCA": { "faseA": "0,20", "faseB": "0,50", "faseC": "0,40", "faseAstatus": 0, "faseBstatus": 0, "faseCstatus": 0 }, "tensaoca": { "faseA": "11,10", "faseB": "11,30", "faseC": "10,20", "faseAstatus": 1, "faseBstatus": 1, "faseCstatus": 1 }, "potenciaativa": "0,00 kW", "energiadodia": "0,00 kWh", "energiaacumulada": "23,20 kWh", "frequencia": "59,85", "statusInversor": 0 }sunweg-3.1.0/tests/responses/list_plant_success_1_response.json000066400000000000000000000001551467730223400251510ustar00rootroot00000000000000{"success":true,"conectadas":[{"id":16925}],"nao_comissionadas":[],"falhas":[],"alertas":[],"atendimento":[]}sunweg-3.1.0/tests/responses/list_plant_success_2_response.json000066400000000000000000000001721467730223400251510ustar00rootroot00000000000000{"success":true,"conectadas":[{"id":16925},{"id":16926}],"nao_comissionadas":[],"falhas":[],"alertas":[],"atendimento":[]}sunweg-3.1.0/tests/responses/list_plant_success_none_response.json000066400000000000000000000001411467730223400257430ustar00rootroot00000000000000{"success":true,"conectadas":[],"nao_comissionadas":[],"falhas":[],"alertas":[],"atendimento":[]}sunweg-3.1.0/tests/responses/month_stats_fail_response.json000066400000000000000000000000641467730223400243650ustar00rootroot00000000000000{ "success": false, "message": "Error message" }sunweg-3.1.0/tests/responses/month_stats_success_response.json000066400000000000000000000230241467730223400251230ustar00rootroot00000000000000{ "graficomes": [ { "energiapordia": 116.9, "prognostico": "111.0322580645161290322580645", "tempoatual": "Wed, 01 May 2024 00:00:00 GMT" }, { "energiapordia": 113.7, "prognostico": "111.0322580645161290322580645", "tempoatual": "Thu, 02 May 2024 00:00:00 GMT" }, { "energiapordia": 112.4, "prognostico": "111.0322580645161290322580645", "tempoatual": "Fri, 03 May 2024 00:00:00 GMT" }, { "energiapordia": 133.3, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sat, 04 May 2024 00:00:00 GMT" }, { "energiapordia": 129.2, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sun, 05 May 2024 00:00:00 GMT" }, { "energiapordia": 86.5, "prognostico": "111.0322580645161290322580645", "tempoatual": "Mon, 06 May 2024 00:00:00 GMT" }, { "energiapordia": 116.2, "prognostico": "111.0322580645161290322580645", "tempoatual": "Tue, 07 May 2024 00:00:00 GMT" }, { "energiapordia": 125.8, "prognostico": "111.0322580645161290322580645", "tempoatual": "Wed, 08 May 2024 00:00:00 GMT" }, { "energiapordia": 127.5, "prognostico": "111.0322580645161290322580645", "tempoatual": "Thu, 09 May 2024 00:00:00 GMT" }, { "energiapordia": 118.7, "prognostico": "111.0322580645161290322580645", "tempoatual": "Fri, 10 May 2024 00:00:00 GMT" }, { "energiapordia": 122.6, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sat, 11 May 2024 00:00:00 GMT" }, { "energiapordia": 123.3, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sun, 12 May 2024 00:00:00 GMT" }, { "energiapordia": 96.3, "prognostico": "111.0322580645161290322580645", "tempoatual": "Mon, 13 May 2024 00:00:00 GMT" }, { "energiapordia": 104.3, "prognostico": "111.0322580645161290322580645", "tempoatual": "Tue, 14 May 2024 00:00:00 GMT" }, { "energiapordia": 120.9, "prognostico": "111.0322580645161290322580645", "tempoatual": "Wed, 15 May 2024 00:00:00 GMT" }, { "energiapordia": 120.8, "prognostico": "111.0322580645161290322580645", "tempoatual": "Thu, 16 May 2024 00:00:00 GMT" }, { "energiapordia": 115.1, "prognostico": "111.0322580645161290322580645", "tempoatual": "Fri, 17 May 2024 00:00:00 GMT" }, { "energiapordia": 118.8, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sat, 18 May 2024 00:00:00 GMT" }, { "energiapordia": 114.5, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sun, 19 May 2024 00:00:00 GMT" }, { "energiapordia": 102.4, "prognostico": "111.0322580645161290322580645", "tempoatual": "Mon, 20 May 2024 00:00:00 GMT" }, { "energiapordia": 106.9, "prognostico": "111.0322580645161290322580645", "tempoatual": "Tue, 21 May 2024 00:00:00 GMT" }, { "energiapordia": 117.1, "prognostico": "111.0322580645161290322580645", "tempoatual": "Wed, 22 May 2024 00:00:00 GMT" }, { "energiapordia": 105.6, "prognostico": "111.0322580645161290322580645", "tempoatual": "Thu, 23 May 2024 00:00:00 GMT" }, { "energiapordia": 99.1, "prognostico": "111.0322580645161290322580645", "tempoatual": "Fri, 24 May 2024 00:00:00 GMT" }, { "energiapordia": 104.4, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sat, 25 May 2024 00:00:00 GMT" }, { "energiapordia": 96.7, "prognostico": "111.0322580645161290322580645", "tempoatual": "Sun, 26 May 2024 00:00:00 GMT" }, { "energiapordia": 110.4, "prognostico": "111.0322580645161290322580645", "tempoatual": "Mon, 27 May 2024 00:00:00 GMT" }, { "energiapordia": 108.6, "prognostico": "111.0322580645161290322580645", "tempoatual": "Tue, 28 May 2024 00:00:00 GMT" }, { "energiapordia": 58.3, "prognostico": "111.0322580645161290322580645", "tempoatual": "2024-05-29" }, { "energiapordia": 0, "prognostico": "111.0322580645161290322580645", "tempoatual": "2024-05-30" }, { "energiapordia": 0, "prognostico": "111.0322580645161290322580645", "tempoatual": "2024-05-31" } ], "graficomescalc": [ { "energiapordia": 0, "prognostico": 0, "tempoatual": "Wed, 01 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Thu, 02 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Fri, 03 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sat, 04 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sun, 05 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Mon, 06 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Tue, 07 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Wed, 08 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Thu, 09 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Fri, 10 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sat, 11 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sun, 12 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Mon, 13 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Tue, 14 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Wed, 15 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Thu, 16 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Fri, 17 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sat, 18 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sun, 19 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Mon, 20 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Tue, 21 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Wed, 22 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Thu, 23 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Fri, 24 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sat, 25 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Sun, 26 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Mon, 27 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "Tue, 28 May 2024 00:00:00 GMT" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "2024-05-29" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "2024-05-30" }, { "energiapordia": 0, "prognostico": 0, "tempoatual": "2024-05-31" } ], "success": true }sunweg-3.1.0/tests/responses/plant_success_alt_response.json000066400000000000000000000011211467730223400245300ustar00rootroot00000000000000{ "success": true, "usinas": { "id": 16925, "nome": "Plant Name", "inversores": [ { "id": 21255, "nome": "Inverter Name", "descricao": "Inverter Description", "esn": "1234ABC", "situacao": 1, "tensaoca": 242, "temperatura": 80 } ] }, "ultimaAtualizacao": null, "AcumuladoPotencia": "25,23 kW", "energiadia": "1,23 kWh", "energiaacumuladanumber": "23.20", "taxaPerformance": 1.48, "KWHporkWp": "1,2", "PerformanceRate": 0.1, "reduz_carbono_total_number": 0.012296, "economia": "12,78" }sunweg-3.1.0/tests/responses/plant_success_response.json000066400000000000000000000011421467730223400236730ustar00rootroot00000000000000{ "success": true, "usinas": { "id": 16925, "nome": "Plant Name", "inversores": [ { "id": 21255, "nome": "Inverter Name", "descricao": "Inverter Description", "esn": "1234ABC", "situacao": 1, "tensaoca": 242, "temperatura": 80 } ] }, "ultimaAtualizacao": "2023-02-25 08:04:22", "AcumuladoPotencia": "25,23 kW", "energiadia": "1,23 kWh", "energiaacumuladanumber": "23.20", "taxaPerformance": 1.48, "KWHporkWp": "1,2", "PerformanceRate": 0.1, "reduz_carbono_total_number": 0.012296, "economia": "12,78" }sunweg-3.1.0/tests/test_api.py000066400000000000000000000435641467730223400163730ustar00rootroot00000000000000"""Test sunweg.api.""" from datetime import date, datetime from os import path import os from unittest import TestCase from unittest.mock import MagicMock, patch import pytest from requests import Response from sunweg.api import ( APIHelper, convert_situation_status, SunWegApiError, separate_value_metric, ) from sunweg.device import Inverter, String from sunweg.util import Status from .common import INVERTER_MOCK, PLANT_MOCK class Api_Test(TestCase): """APIHelper test case.""" responses: dict[str, Response] = {} def setUp(self) -> None: """Set tests up.""" for file in os.listdir(path.join(path.dirname(__file__), "responses")): filename = path.basename(file) with open(path.join(path.dirname(__file__), "responses", file)) as f: response = Response() if filename.startswith("error"): response.status_code = int(filename.split("_")[1]) response.reason = "".join(f.readlines()) else: response.status_code = 200 response._content = "".join(f.readlines()).encode() self.responses[filename] = response def test_convert_situation_status(self) -> None: """Test the conversion from situation to status.""" status_ok: Status = convert_situation_status(1) status_err: Status = convert_situation_status(0) status_wrn: Status = convert_situation_status(2) assert status_ok == Status.OK assert status_err == Status.ERROR assert status_wrn == Status.WARN def test_separate_value_metric_comma(self) -> None: """Test the separation from value and metric of string with comma.""" (value, metric) = separate_value_metric("0,0") assert value == 0 assert metric == "" (value, metric) = separate_value_metric("1,0", "W") assert value == 1.0 assert metric == "W" (value, metric) = separate_value_metric("0,2 kW", "W") assert value == 0.2 assert metric == "kW" def test_separate_value_metric_dot(self) -> None: """Test the separation from value and metric of string with dot.""" (value, metric) = separate_value_metric("0.0") assert value == 0 assert metric == "" (value, metric) = separate_value_metric("1.0", "W") assert value == 1.0 assert metric == "W" (value, metric) = separate_value_metric("0.2 kW", "W") assert value == 0.2 assert metric == "kW" def test_separate_value_metric_none_int(self) -> None: """Test the separation from value and metric of string with dot.""" (value, metric) = separate_value_metric(None) assert value == 0 assert metric == "" (value, metric) = separate_value_metric("1", "W") assert value == 1.0 assert metric == "W" (value, metric) = separate_value_metric("2 kW", "W") assert value == 2.0 assert metric == "kW" def test_error500(self) -> None: """Test error 500.""" with patch( "requests.Session.post", return_value=self.responses["error_500_response.txt"], ): api = APIHelper("user@acme.com", "password") with pytest.raises(SunWegApiError) as e_info: api.authenticate() assert e_info.value.__str__() == "Request failed: " def test_initialize_token(self) -> None: """Test initialize token.""" api = APIHelper(token="token") assert api._token == "token" def test_set_token(self) -> None: """Test set token.""" api = APIHelper(token="token") api.set_token("new_token") assert api._token == "new_token" def test_authenticate_success(self) -> None: """Test authentication success.""" with patch( "requests.Session.post", return_value=self.responses["auth_success_response.json"], ): api = APIHelper("user@acme.com", "password") assert api.authenticate() def test_authenticate_fail_empty_credentials(self) -> None: """Test authentication failed.""" api = APIHelper(None, None) assert not api.authenticate() def test_authenticate_failed(self) -> None: """Test authentication failed.""" with patch( "requests.Session.post", return_value=self.responses["auth_fail_response.json"], ): api = APIHelper("user@acme.com", "password") assert not api.authenticate() def test_list_plants_none_success(self) -> None: """Test list plants with empty plant list.""" with patch( "requests.Session.get", return_value=self.responses["list_plant_success_none_response.json"], ), patch("sunweg.api.APIHelper.plant", return_value=PLANT_MOCK): api = APIHelper("user@acme.com", "password") assert len(api.listPlants()) == 0 def test_list_plants_1_success(self) -> None: """Test list plants with one plant in the list.""" with patch( "requests.Session.get", return_value=self.responses["list_plant_success_1_response.json"], ), patch("sunweg.api.APIHelper.plant", return_value=PLANT_MOCK): api = APIHelper("user@acme.com", "password") assert len(api.listPlants()) == 1 def test_list_plants_2_success(self) -> None: """Test list plants with two plant in the list.""" with patch( "requests.Session.get", return_value=self.responses["list_plant_success_2_response.json"], ), patch("sunweg.api.APIHelper.plant", return_value=PLANT_MOCK): api = APIHelper("user@acme.com", "password") assert len(api.listPlants()) == 2 def test_list_plants_401(self) -> None: """Test list plants with expired token.""" with patch( "requests.Session.post", return_value=self.responses["auth_success_response.json"], ), patch( "requests.Session.get", return_value=self.responses["error_401_response.txt"], ): api = APIHelper("user@acme.com", "password") assert len(api.listPlants()) == 0 def test_plant_success(self) -> None: """Test plant success.""" with patch( "requests.Session.get", return_value=self.responses["plant_success_response.json"], ), patch("sunweg.api.APIHelper.inverter", return_value=INVERTER_MOCK): api = APIHelper("user@acme.com", "password") plant = api.plant(16925) assert plant is not None assert plant.id == 16925 assert plant.name == "Plant Name" assert plant.total_power == 25.23 assert plant.last_update == datetime(2023, 2, 25, 8, 4, 22) assert plant.kwh_per_kwp == 0.0 assert plant.performance_rate == 0.0 assert plant.saving == 12.78 assert plant.today_energy == 1.23 assert plant.today_energy_metric == "kWh" assert plant.total_carbon_saving == 0.012296 assert plant.total_energy == 23.2 assert plant.__str__().startswith("") assert len(plant.inverters) == 1 for inverter in plant.inverters: assert inverter.id == 21255 assert inverter.name == "Inverter Name" assert inverter.frequency == 0 assert inverter.power == 0.0 assert inverter.power_metric == "" assert inverter.power_factor == 0.0 assert inverter.sn == "1234ABC" assert inverter.status == Status.ERROR assert inverter.temperature == 80 assert inverter.today_energy == 0.0 assert inverter.today_energy_metric == "" assert inverter.total_energy == 0.0 assert inverter.total_energy_metric == "" assert not inverter.is_complete def test_plant_success_alt(self) -> None: """Test plant success.""" with patch( "requests.Session.get", return_value=self.responses["plant_success_alt_response.json"], ), patch("sunweg.api.APIHelper.inverter", return_value=INVERTER_MOCK): api = APIHelper("user@acme.com", "password") plant = api.plant(16925) assert plant is not None assert plant.id == 16925 assert plant.name == "Plant Name" assert plant.total_power == 25.23 assert plant.last_update is None assert plant.kwh_per_kwp == 0.0 assert plant.performance_rate == 0.0 assert plant.saving == 12.78 assert plant.today_energy == 1.23 assert plant.today_energy_metric == "kWh" assert plant.total_carbon_saving == 0.012296 assert plant.total_energy == 23.2 assert plant.__str__().startswith("") assert len(plant.inverters) == 1 for inverter in plant.inverters: assert inverter.id == 21255 assert inverter.name == "Inverter Name" assert inverter.frequency == 0 assert inverter.power == 0.0 assert inverter.power_metric == "" assert inverter.power_factor == 0.0 assert inverter.sn == "1234ABC" assert inverter.status == Status.ERROR assert inverter.temperature == 80 assert inverter.today_energy == 0.0 assert inverter.today_energy_metric == "" assert inverter.total_energy == 0.0 assert inverter.total_energy_metric == "" assert not inverter.is_complete def test_plant_401(self) -> None: """Test plant with expired token.""" with patch( "requests.Session.post", return_value=self.responses["auth_success_response.json"], ), patch( "requests.Session.get", return_value=self.responses["error_401_response.txt"], ): api = APIHelper("user@acme.com", "password") assert api.plant(16925) is None def test_inverter_success(self) -> None: """Test inverter success.""" with patch( "requests.Session.get", return_value=self.responses["inverter_success_response.json"], ): api = APIHelper("user@acme.com", "password") inverter = api.inverter(21255) assert inverter is not None assert inverter.id == 21255 assert inverter.name == "Inverter Name" assert inverter.frequency == 59.85 assert inverter.power == 0.0 assert inverter.power_metric == "kW" assert inverter.power_factor == 0.0 assert inverter.sn == "1234ABC" assert inverter.status == Status.OK assert inverter.temperature == 80 assert inverter.today_energy == 0.0 assert inverter.today_energy_metric == "kWh" assert inverter.total_energy == 23.2 assert inverter.today_energy_metric == "kWh" strings: list[String] = [] for mppt in inverter.mppts: assert mppt.__str__().startswith("") assert mppt.name != "" strings.extend(mppt.strings) assert len(strings) == 4 assert len(inverter.phases) == 3 assert inverter.__str__().startswith("") for string in strings: assert string.name != "" assert string.amperage != 0 assert string.voltage != 0 assert string.status == Status.OK assert string.__str__().startswith("") for phase in inverter.phases: assert phase.name != "" assert phase.amperage != 0 assert phase.voltage != 0 assert phase.status_amperage == Status.OK assert phase.status_voltage == Status.ERROR assert phase.__str__().startswith("") def test_inverter_401(self) -> None: """Test inverter with expired token.""" with patch( "requests.Session.post", return_value=self.responses["auth_success_response.json"], ), patch( "requests.Session.get", return_value=self.responses["error_401_response.txt"], ): api = APIHelper("user@acme.com", "password") assert api.inverter(21255) is None def test_complete_inverter_success(self) -> None: """Test complete inverter success.""" with patch( "requests.Session.get", return_value=self.responses["inverter_success_response.json"], ): api = APIHelper("user@acme.com", "password") inverter = Inverter( id=12345, name="Other inverter name", sn="1234ABCD", status=Status.ERROR, temperature=70, ) api.complete_inverter(inverter) assert inverter is not None assert inverter.id == 12345 assert inverter.name == "Other inverter name" assert inverter.frequency == 59.85 assert inverter.power == 0.0 assert inverter.power_factor == 0.0 assert inverter.sn == "1234ABCD" assert inverter.status == Status.ERROR assert inverter.temperature == 70 assert inverter.today_energy == 0.0 assert inverter.total_energy == 23.2 strings: list[String] = [] for mppt in inverter.mppts: assert mppt.__str__().startswith("") assert mppt.name != "" strings.extend(mppt.strings) assert len(strings) == 4 assert len(inverter.phases) == 3 assert inverter.__str__().startswith("") for string in strings: assert string.name != "" assert string.amperage != 0 assert string.voltage != 0 assert string.status == Status.OK assert string.__str__().startswith("") for phase in inverter.phases: assert phase.name != "" assert phase.amperage != 0 assert phase.voltage != 0 assert phase.status_amperage == Status.OK assert phase.status_voltage == Status.ERROR assert phase.__str__().startswith("") def test_complete_inverter_401(self) -> None: """Test complete inverter with expired token.""" with patch( "requests.Session.post", return_value=self.responses["auth_success_response.json"], ), patch( "requests.Session.get", return_value=self.responses["error_401_response.txt"], ): api = APIHelper("user@acme.com", "password") inverter = Inverter( id=12345, name="Other inverter name", sn="1234ABCD", status=Status.ERROR, temperature=70, ) api.complete_inverter(inverter) assert not inverter.is_complete def test_setters(self) -> None: """Test API setters.""" api = APIHelper("user@acme.com", "password") assert api._username == "user@acme.com" assert api._password == "password" api.username = "user1@acme.com" api.password = "password1" assert api._username == "user1@acme.com" assert api._password == "password1" def test_month_stats_fail(self) -> None: """Test month stats with error from server.""" with patch( "requests.Session.get", return_value=self.responses["month_stats_fail_response.json"], ): api = APIHelper("user@acme.com", "password") plant = MagicMock() plant.id = 1 with pytest.raises(SunWegApiError) as e_info: api.month_stats_production(2013, 12, plant) assert e_info.value.__str__() == "Error message" def test_month_stats_401(self) -> None: """Test month stats with data from server with expired token.""" with patch( "requests.Session.post", return_value=self.responses["auth_success_response.json"], ), patch( "requests.Session.get", return_value=self.responses["error_401_response.txt"], ): api = APIHelper("user@acme.com", "password") plant = MagicMock() plant.id = 1 stats = api.month_stats_production(2023, 12, plant) assert isinstance(stats, list) assert len(stats) == 0 def test_month_stats_success(self) -> None: """Test month stats with data from server.""" with patch( "requests.Session.get", return_value=self.responses["month_stats_success_response.json"], ): api = APIHelper("user@acme.com", "password") plant = MagicMock() plant.id = 1 stats = api.month_stats_production(2023, 12, plant) assert len(stats) > 0 i: int = 1 for stat in stats: assert stat.date == date(2024, 5, i) assert isinstance(stat.production, float) assert stat.prognostic == 111.03225806451613 assert stat.__str__().startswith( "" ) i += 1