diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..a304f8f --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,40 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python package + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install flake8 pytest + 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 +# - name: Test with pytest +# run: | +# pytest diff --git a/.gitignore b/.gitignore index 0d8c32b..4934ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,python -# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,python +# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode ### Python ### # Byte-compiled / optimized / DLL files @@ -192,4 +192,4 @@ pyrightconfig.json .history .ionide -# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,python \ No newline at end of file +# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode diff --git a/README.md b/README.md index 56d20b3..bec8f18 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,12 @@ Fetches current electricity prices and outages for specified client id. ### 4. python3 run.py -## How to use in HomeAssistant: +## CICD: -### 1. Copy contents of configuration.yaml to the configuration.yaml file of your HA install +### Using GitLab Workflow with feature branch approach -### 2. Change your client id. +### Create branch based on develop, named based on developed feature, push, create Pull Request -### 3. Restart HomeAssistant +### On PR for verification, the source code is ran with different versions of Python (3.8, 3.9, 3.10) and linted with flake8. + +### Develop -> Main branch merges are done when the develop branch is stable and after a version/state tag on the develop branch state. diff --git a/bg_ED_data/__init__.py b/bg_ED_data/__init__.py index 4323b31..902ab2a 100644 --- a/bg_ED_data/__init__.py +++ b/bg_ED_data/__init__.py @@ -1,2 +1,31 @@ #!/usr/bin/env python -# -*- coding: utf8 -*- \ No newline at end of file +# -*- coding: utf8 -*- + +#region File Attributes + +__author__ = "Orlin Dimitrov" +"""Author of the file.""" + +__copyright__ = "" +"""Copyrighted""" + +__credits__ = [] +"""Credits""" + +__license__ = "" +"""License +@see """ + +__version__ = "1.0.0" +"""Version of the file.""" + +__maintainer__ = ["Orlin Dimitrov", "Martin Maslyankov", "Nikola Atanasov"] +"""Name of the maintainer.""" + +__email__ = "" +"""E-mail of the author.""" + +__class_name__ = "" +"""Provider class name.""" + +#endregion diff --git a/bg_ED_data/__main__.py b/bg_ED_data/__main__.py index 4f23fc2..e12ee3d 100644 --- a/bg_ED_data/__main__.py +++ b/bg_ED_data/__main__.py @@ -1,8 +1,9 @@ #!/usr/bin/env python # -*- coding: utf8 -*- -# from providers.electrohold.electrohold import Electrohold as Provider -from providers.erp_sever.erp_sever import ERPSever as Provider +import argparse + +from bg_ED_data.providers.factory import Factory #region File Attributes @@ -28,16 +29,27 @@ __email__ = "" """E-mail of the author.""" -__class_name__ = "BaseProvider" +__class_name__ = "" """Provider class name.""" #endregion def main(): + """Main function. + """ + + # Create parser. + parser = argparse.ArgumentParser() + + parser.add_argument("--provider", type=str, default="erp_sever", help="ERP Sever") + + # Take arguments. + args = parser.parse_args() + + provider = Factory.create(args.provider) print("Starting work...") - provider = Provider() ids = ['300066244165', '123456789101'] for identifier in ids: diff --git a/bg_ED_data/providers/erp_sever/__main__.py b/bg_ED_data/exceptions/__init__.py similarity index 53% rename from bg_ED_data/providers/erp_sever/__main__.py rename to bg_ED_data/exceptions/__init__.py index c2c200c..902ab2a 100644 --- a/bg_ED_data/providers/erp_sever/__main__.py +++ b/bg_ED_data/exceptions/__init__.py @@ -1,10 +1,6 @@ #!/usr/bin/env python # -*- coding: utf8 -*- -import sys - -from erp_sever.erp_sever import ERPSever as Provider - #region File Attributes __author__ = "Orlin Dimitrov" @@ -33,20 +29,3 @@ """Provider class name.""" #endregion - -def main(): - - print('cmd entry:', sys.argv) - - provider = Provider() - ids = ['300066244165', '123456789101'] - - for identifier in ids: - print("Electricity outages data:") - print(provider.get_outages(identifier=identifier)) - - print("Electricity prices data:") - print(provider.get_prices()) - -if __name__ == "__main__": - main() diff --git a/bg_ED_data/providers/__init__.py b/bg_ED_data/providers/__init__.py index 4323b31..902ab2a 100644 --- a/bg_ED_data/providers/__init__.py +++ b/bg_ED_data/providers/__init__.py @@ -1,2 +1,31 @@ #!/usr/bin/env python -# -*- coding: utf8 -*- \ No newline at end of file +# -*- coding: utf8 -*- + +#region File Attributes + +__author__ = "Orlin Dimitrov" +"""Author of the file.""" + +__copyright__ = "" +"""Copyrighted""" + +__credits__ = [] +"""Credits""" + +__license__ = "" +"""License +@see """ + +__version__ = "1.0.0" +"""Version of the file.""" + +__maintainer__ = ["Orlin Dimitrov", "Martin Maslyankov", "Nikola Atanasov"] +"""Name of the maintainer.""" + +__email__ = "" +"""E-mail of the author.""" + +__class_name__ = "" +"""Provider class name.""" + +#endregion diff --git a/bg_ED_data/providers/base/__init__.py b/bg_ED_data/providers/base/__init__.py index 4323b31..902ab2a 100644 --- a/bg_ED_data/providers/base/__init__.py +++ b/bg_ED_data/providers/base/__init__.py @@ -1,2 +1,31 @@ #!/usr/bin/env python -# -*- coding: utf8 -*- \ No newline at end of file +# -*- coding: utf8 -*- + +#region File Attributes + +__author__ = "Orlin Dimitrov" +"""Author of the file.""" + +__copyright__ = "" +"""Copyrighted""" + +__credits__ = [] +"""Credits""" + +__license__ = "" +"""License +@see """ + +__version__ = "1.0.0" +"""Version of the file.""" + +__maintainer__ = ["Orlin Dimitrov", "Martin Maslyankov", "Nikola Atanasov"] +"""Name of the maintainer.""" + +__email__ = "" +"""E-mail of the author.""" + +__class_name__ = "" +"""Provider class name.""" + +#endregion diff --git a/bg_ED_data/providers/electrohold/__init__.py b/bg_ED_data/providers/electrohold/__init__.py index 4323b31..902ab2a 100644 --- a/bg_ED_data/providers/electrohold/__init__.py +++ b/bg_ED_data/providers/electrohold/__init__.py @@ -1,2 +1,31 @@ #!/usr/bin/env python -# -*- coding: utf8 -*- \ No newline at end of file +# -*- coding: utf8 -*- + +#region File Attributes + +__author__ = "Orlin Dimitrov" +"""Author of the file.""" + +__copyright__ = "" +"""Copyrighted""" + +__credits__ = [] +"""Credits""" + +__license__ = "" +"""License +@see """ + +__version__ = "1.0.0" +"""Version of the file.""" + +__maintainer__ = ["Orlin Dimitrov", "Martin Maslyankov", "Nikola Atanasov"] +"""Name of the maintainer.""" + +__email__ = "" +"""E-mail of the author.""" + +__class_name__ = "" +"""Provider class name.""" + +#endregion diff --git a/bg_ED_data/providers/electrohold/configuration.yaml b/bg_ED_data/providers/electrohold/configuration.yaml deleted file mode 100644 index ad527b8..0000000 --- a/bg_ED_data/providers/electrohold/configuration.yaml +++ /dev/null @@ -1,88 +0,0 @@ - ## - ## ------------## CEZ tariffs scraper and outages scanner ## ------------------------------------------------------------- - # https://github.com/maslyankov/bg_electricity_distributors_api.git - -sensor: - # Chez / electrohold.bg - Start - # Scrape tariffs - - platform: scrape - resource: https://ermzapad.bg/bg/za-klienta/ceni-i-nachini-na-plashane/ceni-za-dostp-i-prenos/ - name: Electricity price (access cost) - select: 'span[style="color:#333333"]' - value_template: '{{( value | replace (",", ".") | float * 1.2 )| round(5) }}' - index: 5 - unit_of_measurement: "BGN/kWh" - scan_interval: 21600 # every 6 hours - - platform: scrape - resource: https://ermzapad.bg/bg/za-klienta/ceni-i-nachini-na-plashane/ceni-za-dostp-i-prenos/ - name: Electricity price (transmission cost for low voltage dist net) - select: 'span[style="color:#333333"]' - value_template: '{{( value | replace (",", ".") | float * 1.2 )| round(5) }}' - index: 9 - unit_of_measurement: "BGN/kWh" - scan_interval: 21600 # every 6 hours - - platform: scrape - resource: https://electrohold.bg/bg/sales/domakinstva/snabdyavane-po-regulirani-ceni/ - name: "CEZ Night tariff" - select: 'td[class="xl65"]' - value_template: '{{( value | replace (",", ".") | float * 1.2 )| round(5) }}' - index: 3 - unit_of_measurement: "BGN/kWh" - scan_interval: 21600 # every 6 hours - - platform: scrape - resource: https://electrohold.bg/bg/sales/domakinstva/snabdyavane-po-regulirani-ceni/ - name: "CEZ Day tariff" - select: 'td[class="xl65"]' - value_template: '{{( value | replace (",", ".") | float * 1.2 )| round(5) }}' - index: 1 - unit_of_measurement: "BGN/kWh" - scan_interval: 21600 # every 6 hours - # Add tax to el tariffs and sum them - - - platform: template - sensors: - cez_total_price_day: - friendly_name: "Electricity price (day)" - unit_of_measurement: "BGN/kWh" - value_template: "{{ states('sensor.cez_day_tariff') | float + states('sensor.electricity_price_access_cost') | float + states('sensor.electricity_price_transmission_cost_for_low_voltage_dist_net') | float | round(5) }}" - cez_total_price_night: - friendly_name: "Electricity price (night)" - unit_of_measurement: "BGN/kWh" - value_template: "{{ states('sensor.cez_night_tariff') | float + states('sensor.electricity_price_access_cost') | float + states('sensor.electricity_price_transmission_cost_for_low_voltage_dist_net') | float | round(5) }}" - electricity_price: - friendly_name: "Electricity price" - unit_of_measurement: "BGN/kWh" - value_template: >- - {% if '07:00:00' < now().strftime('%T') < '22:00:00' %} - {{ states('sensor.cez_total_price_day') }} - {% else %} - {{ states('sensor.cez_total_price_night') }} - {% endif %} - icon_template: >- - {% if '07:00:00' < now().strftime('%T') < '22:00:00' %} - mdi:weather-sunny - {% else %} - mdi:weather-night - {% endif %} - - # Get electricity outages data - - platform: rest - name: "Electricity - Current Outage" - resource: https://info.electrohold.bg/webint/vok/avplan.php - method: POST - payload: "action=viewitn&itn=1234567890" # Change client id - value_template: '{{ value.encode().decode("utf-8-sig").split("При липса")[0] }}' - scan_interval: 1800 # every 1/2 hour - headers: - Content-Type: "application/x-www-form-urlencoded; charset=utf-8" - - - platform: rest - name: "Electricity - Planned Outages" - resource: https://info.electrohold.bg/webint/vok/avplan.php - method: POST - payload: "action=viewitn_plan&itn=1234567890" # Change client id - value_template: '{{ value.encode().decode("utf-8-sig") }}' - scan_interval: 1800 # every 1/2 hour - headers: - Content-Type: "application/x-www-form-urlencoded; charset=utf-8" - # Chez / electrohold.bg - end diff --git a/bg_ED_data/providers/electrohold/electrohold.py b/bg_ED_data/providers/electrohold/electrohold.py index f77cbbc..915fb5b 100644 --- a/bg_ED_data/providers/electrohold/electrohold.py +++ b/bg_ED_data/providers/electrohold/electrohold.py @@ -3,12 +3,14 @@ import json -from providers.base.base_provider import BaseProvider -from utils.html_parser import HTMLTableParser +from bg_ED_data.providers.base.base_provider import BaseProvider +from bg_ED_data.utils.html_parser import HTMLTableParser -# Suppress ssl warnings import requests -requests.urllib3.disable_warnings(requests.urllib3.exceptions.InsecureRequestWarning) +import urllib3 + +# Suppress ssl warnings +# urllib3.disable_warnings(requests.exceptions.SSLError) from webbrowser import get diff --git a/bg_ED_data/providers/erp_sever/erp_sever.py b/bg_ED_data/providers/erp_sever/erp_sever.py index 1a9a174..78d810e 100644 --- a/bg_ED_data/providers/erp_sever/erp_sever.py +++ b/bg_ED_data/providers/erp_sever/erp_sever.py @@ -1,11 +1,12 @@ #!/usr/bin/env python # -*- coding: utf8 -*- -from providers.base.base_provider import BaseProvider +from bg_ED_data.providers.base.base_provider import BaseProvider # Suppress ssl warnings import requests -requests.urllib3.disable_warnings(requests.urllib3.exceptions.InsecureRequestWarning) + +# requests.urllib3.disable_warnings(requests.urllib3.exceptions.InsecureRequestWarning) #region File Attributes @@ -39,9 +40,38 @@ class ERPSever(BaseProvider): def get_outages(self, **kwargs): - url = "https://erpsever.bg/bg/profil/xhr/?method=get_interruptions®ion_id=2&type=for_next_48_hours&offset=0&archive_from_date=&archive_to_date=" - response = requests.get(url) + cookies = { + 'STDXFWSID': '3prf6nchustns3phuq1om0o881', + } + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0', + 'Accept': 'application/json, text/javascript, */*; q=0.01', + 'Accept-Language': 'en-US,en;q=0.5', + # 'Accept-Encoding': 'gzip, deflate, br', + 'Referer': 'https://www.erpsever.bg/bg/prekysvanija', + 'Content-Type': 'application/json; charset=utf-8', + 'X-Requested-With': 'XMLHttpRequest', + 'Connection': 'keep-alive', + # 'Cookie': 'STDXFWSID=3prf6nchustns3phuq1om0o881', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + } + + params = { + 'method': 'get_interruptions', + 'region_id': '2', # TODO: To be an argument of the method. Create enum for all IDs. + 'type': 'for_next_48_hours', # TODO: To be an argument of the method. + 'offset': '0', + 'archive_from_date': '', # TODO: To be an argument of the method. + 'archive_to_date': '', # TODO: To be an argument of the method. + } + + response = requests.get('https://www.erpsever.bg/bg/profil/xhr/', params=params, cookies=cookies, headers=headers) + response_data_raw = response.text.encode().decode('utf-8-sig') + print(response_data_raw) diff --git a/bg_ED_data/providers/factory.py b/bg_ED_data/providers/factory.py new file mode 100644 index 0000000..3e04775 --- /dev/null +++ b/bg_ED_data/providers/factory.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf8 -*- + +from bg_ED_data.providers.electrohold.electrohold import Electrohold +from bg_ED_data.providers.erp_sever.erp_sever import ERPSever + +#region File Attributes + +__author__ = "Orlin Dimitrov" +"""Author of the file.""" + +__copyright__ = "" +"""Copyrighted""" + +__credits__ = [] +"""Credits""" + +__license__ = "" +"""License +@see """ + +__version__ = "1.0.0" +"""Version of the file.""" + +__maintainer__ = ["Orlin Dimitrov", "Martin Maslyankov", "Nikola Atanasov"] +"""Name of the maintainer.""" + +__email__ = "" +"""E-mail of the author.""" + +__class_name__ = "ERPSever" +"""Provider class name.""" + +#endregion + +class Factory(): + """Providers factory. + """ + + @staticmethod + def create(provider: str): + """Providers creator method. + + Args: + provider (str): The name of wanted provider. + + Raises: + NotImplementedError: It raise when unknown provider name is passed. + + Returns: + any: Instance of the target provider. + """ + provider_instance = None + + if provider == "erp_sever": + provider_instance = ERPSever() + + elif provider == "electro_hold": + provider_instance = Electrohold() + + else: + raise NotImplementedError("provider not implemented.") + + return provider_instance diff --git a/setup.py b/setup.py index ff77645..f185671 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,12 @@ #!/usr/bin/env python # -*- coding: utf8 -*- +import sys + from setuptools import find_packages, setup +import bg_ED_data + #region File Attributes __author__ = "Orlin Dimitrov" @@ -29,16 +33,47 @@ #endregion +def long_description(): + """Long description reader. + + Returns: + str: Long description text. + """ + with open('README.md', encoding='utf-8') as file: + return file.read() + +install_requires = ["certifi==2022.12.7", "charset-normalizer==3.1.0", + "idna==3.4", "requests==2.28.2", "urllib3==1.26.15"], + setup( name="bg_ED_data", - packages=find_packages(include=["bg_ED_data"]), + packages=find_packages(include=["bg_ED_data", 'bg_ED_data.*']), + entry_points={ + 'console_scripts': [ + 'bg_ED_data = bg_ED_data.__main__:main' + ] + }, version=__version__, description="Bulgaria electro distribution data provider.", + long_description=long_description(), + long_description_content_type='text/markdown', author=__author__, license=__license__, - install_requires=[], + author_email=__email__, + python_requires='>=3.7', + install_requires=install_requires, setup_requires=[], - tests_require=["certifi==2022.12.7", "charset-normalizer==3.1.0", - "idna==3.4", "requests==2.28.2", "urllib3==1.26.15"], + tests_require=[], test_suite="", + project_urls={ + 'GitHub': 'https://github.com/wectrl-io/bg_ED_data', + }, + classifiers=[ + 'Development Status :: 1 - Debug', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3 :: Only', + 'Environment :: Console', + 'Intended Audience :: Developers', + ], + # package_data={'example.path.lob.name': ['file_name.extension']} )