├── .coveragerc ├── .dockerignore ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1-question.md │ ├── 2-bug-report.md │ └── 3-feature-request.md ├── PULL_REQUEST_TEMPLATE ├── dependabot.yml └── workflows │ ├── publish.yml │ └── pull_request.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── IEC.postman_collection.json ├── LICENSE ├── Makefile ├── POSTMAN.md ├── README.md ├── README_DEV.md ├── docker-compose.yml ├── example.py ├── iec_api ├── __init__.py ├── commons.py ├── const.py ├── data.py ├── fault_portal_data.py ├── fault_portal_models │ ├── fault_portal_base.py │ ├── outages.py │ └── user_profile.py ├── iec_client.py ├── login.py ├── masa_api_models │ ├── building_options.py │ ├── cities.py │ ├── equipment.py │ ├── lookup.py │ ├── order_lookup.py │ ├── titles.py │ ├── user_profile.py │ └── volt_levels.py ├── masa_data.py ├── models │ ├── account.py │ ├── contract.py │ ├── contract_check.py │ ├── customer.py │ ├── device.py │ ├── device_identity.py │ ├── device_type.py │ ├── efs.py │ ├── electric_bill.py │ ├── error_response.py │ ├── exceptions.py │ ├── get_pdf.py │ ├── invoice.py │ ├── jwt.py │ ├── meter_reading.py │ ├── models_commons.py │ ├── okta_errors.py │ ├── outages.py │ ├── remote_reading.py │ ├── response_descriptor.py │ └── send_consumption_to_mail.py ├── static_data.py └── usage_calculator │ ├── __init__.py │ ├── calculator.py │ ├── consumption.py │ ├── electric_device.py │ ├── get_calculator_response.py │ └── rates.py ├── logging.conf ├── pyproject.toml ├── renovate.json └── tests ├── __init__.py ├── commons_test.py └── e2e_test.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = tests/*, example.py -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | .pytest_cache 4 | .ruff_cache 5 | *.md 6 | !README.md 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @GuyKh -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: guykh # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: guykh # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Questions about this project 4 | labels: question 5 | --- 6 | 7 | ### Checklist 8 | - [ ] There are no similar issues or pull requests about this question. 9 | 10 | ### Describe your question 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug to help improve this project 4 | labels: bug 5 | --- 6 | ### Describe the bug 7 | 8 | 9 | ### To reproduce 10 | 11 | 12 | ### Expected behavior 13 | 14 | 15 | ### Actual behavior 16 | 17 | 18 | 19 | ### Additional context -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project. 4 | labels: feature, enhancement 5 | --- 6 | 7 | ### Checklist 8 | 9 | 10 | 11 | - [ ] There are no similar issues or pull requests for this yet. 12 | 13 | ### Is your feature related to a problem? Please describe. 14 | 15 | 18 | 19 | ## Describe the solution you would like. 20 | 21 | 25 | 26 | ## Describe alternatives you considered 27 | 28 | 30 | 31 | ## Additional context 32 | 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## What changes do you are proposing? 2 | 3 | ## How did you test these changes? 4 | 5 | **Closing issues** 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "[0-9]+.[0-9]+.[0-9]+" 7 | - "[0-9]+.[0-9]+.[0-9]+a[0-9]+" 8 | - "[0-9]+.[0-9]+.[0-9]+b[0-9]+" 9 | - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" 10 | 11 | env: 12 | PACKAGE_NAME: "iec-api" 13 | OWNER: "GuyKh" 14 | 15 | jobs: 16 | details: 17 | runs-on: ubuntu-latest 18 | outputs: 19 | new_version: ${{ steps.release.outputs.new_version }} 20 | suffix: ${{ steps.release.outputs.suffix }} 21 | tag_name: ${{ steps.release.outputs.tag_name }} 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Extract tag and Details 26 | id: release 27 | run: | 28 | if [ "${{ github.ref_type }}" = "tag" ]; then 29 | TAG_NAME=${GITHUB_REF#refs/tags/} 30 | NEW_VERSION=$(echo $TAG_NAME | awk -F'-' '{print $1}') 31 | SUFFIX=$(echo $TAG_NAME | grep -oP '[a-z]+[0-9]+' || echo "") 32 | echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" 33 | echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT" 34 | echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" 35 | echo "Version is $NEW_VERSION" 36 | echo "Suffix is $SUFFIX" 37 | echo "Tag name is $TAG_NAME" 38 | else 39 | echo "No tag found" 40 | exit 1 41 | fi 42 | 43 | check_pypi: 44 | needs: details 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Fetch information from PyPI 48 | run: | 49 | response=$(curl -s https://pypi.org/pypi/${{ env.PACKAGE_NAME }}/json || echo "{}") 50 | latest_previous_version=$(echo $response | jq --raw-output "select(.releases != null) | .releases | keys_unsorted | last") 51 | if [ -z "$latest_previous_version" ]; then 52 | echo "Package not found on PyPI." 53 | latest_previous_version="0.0.0" 54 | fi 55 | echo "Latest version on PyPI: $latest_previous_version" 56 | echo "latest_previous_version=$latest_previous_version" >> $GITHUB_ENV 57 | 58 | - name: Compare versions and exit if not newer 59 | run: | 60 | NEW_VERSION=${{ needs.details.outputs.new_version }} 61 | LATEST_VERSION=$latest_previous_version 62 | if [ "$(printf '%s\n' "$LATEST_VERSION" "$NEW_VERSION" | sort -rV | head -n 1)" != "$NEW_VERSION" ] || [ "$NEW_VERSION" == "$LATEST_VERSION" ]; then 63 | echo "The new version $NEW_VERSION is not greater than the latest version $LATEST_VERSION on PyPI." 64 | exit 1 65 | else 66 | echo "The new version $NEW_VERSION is greater than the latest version $LATEST_VERSION on PyPI." 67 | fi 68 | 69 | setup_and_build: 70 | needs: [details, check_pypi] 71 | runs-on: ubuntu-latest 72 | steps: 73 | - uses: actions/checkout@v4 74 | 75 | - name: Set up Python 76 | uses: actions/setup-python@v5 77 | with: 78 | python-version: "3.11" 79 | 80 | - name: Install Poetry 81 | run: | 82 | curl -sSL https://install.python-poetry.org | python3 - 83 | echo "$HOME/.local/bin" >> $GITHUB_PATH 84 | 85 | - name: Set project version with Poetry 86 | run: | 87 | poetry version ${{ needs.details.outputs.new_version }} 88 | 89 | - name: Install dependencies 90 | run: poetry install --sync --no-interaction 91 | 92 | - name: Build source publish 93 | env: 94 | PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 95 | run: | 96 | poetry config pypi-token.pypi $PYPI_TOKEN 97 | poetry publish --build 98 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: pull_request 2 | on: [pull_request] 3 | jobs: 4 | run_tests_and_lint: 5 | name: "Run Tests and Lint" 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo "Running build and test for ${{ github.ref }} branch" 9 | 10 | - name: Check out repository code 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 1 14 | 15 | - name: Installing the project 16 | run: make docker/install 17 | 18 | - name: Running Lint 19 | run: make docker/lint 20 | 21 | #- name: Running tests 22 | # run: make docker/test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .DS_Store 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | pip-wheel-metadata/ 25 | share/python-wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .nox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | *.py,cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # Poetry lock file 133 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 134 | poetry.lock 135 | 136 | # Visual Studio Code 137 | .vscode 138 | 139 | # Ruff Settings 140 | .ruff_cache -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | # Ruff version. 4 | rev: v0.2.2 5 | hooks: 6 | # Run the linter. 7 | - id: ruff 8 | args: [ --fix ] 9 | # Run the formatter. 10 | - id: ruff-format 11 | 12 | - repo: https://github.com/compilerla/conventional-pre-commit 13 | rev: v3.1.0 14 | hooks: 15 | - id: conventional-pre-commit 16 | stages: [ commit-msg ] 17 | args: [ ] # optional: list of Conventional Commits types to allow e.g. [feat, fix, ci, chore, test] -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | To be a truly great community, `py-iec` needs to welcome developers from all walks of life, 4 | with different backgrounds, and with a wide range of experience. A diverse and friendly 5 | community will have more great ideas, more unique perspectives, and produce more great 6 | code. We will work diligently to make the `py-iec` community welcoming to everyone. 7 | 8 | To give clarity of what is expected of our members, `py-iec` has adopted the code of conduct 9 | defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source 10 | communities, and we think it articulates our values well. The full text is copied below: 11 | 12 | ### Contributor Code of Conduct v2.1 13 | 14 | As contributors and maintainers of this project, and in the interest of fostering an open and 15 | welcoming community, we pledge to respect all people who contribute through reporting 16 | issues, posting feature requests, updating documentation, submitting pull requests or patches, 17 | and other activities. 18 | 19 | We are committed to making participation in this project a harassment-free experience for 20 | everyone, regardless of level of experience, gender, gender identity and expression, sexual 21 | orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or 22 | nationality. 23 | 24 | Examples of unacceptable behavior by participants include: 25 | - The use of sexualized language or imagery 26 | - Personal attacks 27 | - Trolling or insulting/derogatory comments 28 | - Public or private harassment 29 | - Publishing other’s private information, such as physical or electronic addresses, without explicit permission 30 | - Other unethical or unprofessional conduct 31 | 32 | Project maintainers have the right and responsibility to remove, edit, or reject comments, 33 | commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of 34 | Conduct, or to ban temporarily or permanently any contributor for other behaviors that they 35 | deem inappropriate, threatening, offensive, or harmful. 36 | 37 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and 38 | consistently applying these principles to every aspect of managing this project. Project 39 | maintainers who do not follow or enforce the Code of Conduct may be permanently removed 40 | from the project team. 41 | 42 | This code of conduct applies both within project spaces and in public spaces when an 43 | individual is representing the project or its community. 44 | 45 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 46 | contacting a project maintainer. All complaints will be reviewed and 47 | investigated and will result in a response that is deemed necessary and appropriate to the 48 | circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter 49 | of an incident. 50 | 51 | *This policy is adapted from the Contributor Code of Conduct [version 2.1.0](https://www.contributor-covenant.org/version/2/1/code_of_conduct).* 52 | 53 | ### Reporting 54 | 55 | A working group of community members is committed to promptly addressing any reported issues. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) 2 | 3 | Welcome, please read with careful and patience our manifest and coding style. 4 | 5 | # Be pythonic! 6 | 7 | ``` 8 | Beautiful is better than ugly. 9 | Explicit is better than implicit. 10 | Simple is better than complex. 11 | Complex is better than complicated. 12 | Flat is better than nested. 13 | Sparse is better than dense. 14 | Readability counts. 15 | Special cases aren't special enough to break the rules. 16 | Although practicality beats purity. 17 | Errors should never pass silently. 18 | Unless explicitly silenced. 19 | In the face of ambiguity, refuse the temptation to guess. 20 | There should be one-- and preferably only one --obvious way to do it. 21 | Although that way may not be obvious at first unless you're Dutch. 22 | Now is better than never. 23 | Although never is often better than *right* now. 24 | If the implementation is hard to explain, it's a bad idea. 25 | If the implementation is easy to explain, it may be a good idea. 26 | Namespaces are one honking great idea -- let's do more of those! 27 | ``` 28 | [The zen of python - PEP20](https://www.python.org/dev/peps/pep-0020/) 29 | 30 | # Manifest 31 | 32 | - First of all: **Be pythonic** :) 33 | - [DRY](http://deviq.com/don-t-repeat-yourself/) - Don't repeat yourself. 34 | - [KISS](https://deviq.com/keep-it-simple/) - Keep it simple stupid. 35 | 36 | 37 | # Coding Style 38 | 39 | We are using [Ruff](https://github.com/astral-sh/ruff) to manage the coding style [rules](https://beta.ruff.rs/docs/rules/). 40 | 41 | Rule | Description 42 | --- | --- 43 | E,W | [pycode style](https://pypi.org/project/pycodestyle/) 44 | F | [pyflakes](https://pypi.org/project/pyflakes/) 45 | I | [isort](https://pypi.org/project/isort/) 46 | N | [pep8-naming](https://pypi.org/project/pep8-naming/) 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.13.2-slim AS base 2 | 3 | WORKDIR /src 4 | 5 | COPY pyproject.toml README.md . 6 | COPY iec_api ./iec_api 7 | RUN pip install poetry 8 | 9 | FROM base AS dependencies 10 | RUN poetry install --no-dev 11 | 12 | FROM base AS development 13 | RUN poetry install 14 | COPY . . 15 | 16 | FROM dependencies AS production 17 | COPY iec_api src 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Marciel Torres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME="iec-api" 2 | 3 | ################################ 4 | # COMMANDS TO RUN LOCALLY 5 | ################################ 6 | 7 | local/install: 8 | poetry install 9 | 10 | local/tests: 11 | poetry run pytest --cov-report=html --cov-report=term --cov . 12 | 13 | local/lint: 14 | poetry run ruff check . 15 | poetry run ruff . --fix --exit-non-zero-on-fix 16 | 17 | local/lint/fix: 18 | poetry run black . 19 | 20 | local/run: 21 | poetry run python src/main.py 22 | 23 | 24 | ############################################ 25 | # COMMANDS TO RUN USING DOCKER (RECOMMENDED) 26 | ############################################ 27 | 28 | docker/install: 29 | docker compose build ${APP_NAME} 30 | 31 | docker/up: 32 | docker compose up -d 33 | 34 | docker/down: 35 | docker compose down --remove-orphans 36 | 37 | docker/test: 38 | docker compose run ${APP_NAME} poetry run pytest --cov-report=html --cov-report=term --cov . 39 | 40 | docker/lint: 41 | docker compose run ${APP_NAME} poetry run ruff check . 42 | 43 | docker/lint/fix: 44 | docker compose run ${APP_NAME} poetry run ruff . --fix --exit-non-zero-on-fix 45 | 46 | docker/run: 47 | docker compose run ${APP_NAME} poetry run python src/main.py 48 | 49 | ################## 50 | # HEPFUL COMMANDS 51 | ################## 52 | -------------------------------------------------------------------------------- /POSTMAN.md: -------------------------------------------------------------------------------- 1 | # Calling the API with Postman 2 | 3 | I've done a lot of work exposing and automating the work for you if you want to manually use Postman to call IEC API. 4 | 5 | You can download and import the [IEC Postman Collection](https://github.com/GuyKh/py-iec-api/blob/main/IEC.postman_collection.json) and follow this guide: 6 | 7 | # Step 1: Setup your UserID 8 | In the Collection Variable configuration - adjust your `user_id` variable to match your User ID (תעודת זהות). 9 | 10 | # Step 2: Obtaining an `id_token` 11 | You can do this in multiple ways: 12 | 13 | ## Step 2A: Get refresh_token from [HomeAssistant Custom Component](https://github.com/GuyKh/iec-custom-component) 14 | If you're using HomeAssistant Component, you can fetch the `refresh_token` and use it to get a fresh `id_token` 15 | 16 | In your Home Assistant directory - run: 17 | ``` 18 | $ cat .storage/core.config_entries | grep iec -B5 -A20 | grep refresh_token 19 | "refresh_token": "55rSzGfPGY3i9iONu_J_FGfYhWdZsszM_abcdEFG", 20 | ``` 21 | 22 | Copy the value of this token to `refresh_token` variable in Collection Variables. 23 | Run step `OAuth/Refresh Token`. 24 | If the call was successfull, the `id_token` should be automatically filled. 25 | 26 | ## Step 2B: Refresh The Token 27 | If you already configured `refresh_token` sometime before, you can reuse it (up to some time after creation). 28 | Simply run step `OAuth/Refresh Token`. 29 | If the call was successfull, the `id_token` should be automatically filled. 30 | 31 | ## Step 2C: Manually go through the Login Process 32 | Run the following Postman calls in this order: 33 | - `OAuth/Step 1: Factor ID` 34 | - `OAuth/Step 2: Send OTP` 35 | - At this point you should have gotten your OTP token by SMS or Email - fill it in `otpCode` variable in Collection Variables 36 | - `OAuth/Step 3: Verify OTP` 37 | - `OAuth/Step 4: Authorize Session` 38 | - `OAuth/Step 5: Get AccessToken` 39 | And at this point you should have gotten a response including a `refresh_token` and the desired `id_token` 40 | 41 | # **Important** - if any of the next calls would return an _401 Unauthorized_ response, repeat Step2 to get new `id_token` 42 | 43 | # Step 3: Fill `bp_number` 44 | Run `Account` or `Customer`. 45 | For `Account`, the `bp_number` variable is the `accountNumber` value. 46 | For `Customer`, it's just the `bpNumber` value. 47 | One way or another, the tests should populate the value automatically in the Collection Variables 48 | 49 | # Step 4: Fill `contract_id` 50 | Run `Contracts` 51 | Value for `contract_id` is from field `contractId` and (like before) should be populated automatically 52 | 53 | # Step 5: Fill `device_id` 54 | Run `Devices`. 55 | Value for `device_id` is from field `deviceNumber` and (like before) should be populated automatically 56 | 57 | # Step 6: Enjoy 58 | Now you have all the required variables for all the calls filled and you can run it by yourself -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iec-api 2 | 3 | A python wrapper for Israel Electric Company API 4 | 5 | ## Module Usage 6 | 7 | ```python 8 | from iec_api import iec_client as iec 9 | 10 | client = iec.IecClient("123456789") 11 | try: 12 | await client.manual_login() # login with user inputs 13 | except iec.exceptions.IECError as err: 14 | logger.error(f"Failed Login: (Code {err.code}): {err.error}") 15 | raise 16 | 17 | customer = await client.get_customer() 18 | print(customer) 19 | 20 | contracts = await client.get_contracts() 21 | for contract in contracts: 22 | print(contract) 23 | 24 | reading = await client.get_last_meter_reading(customer.bp_number, contracts[0].contract_id) 25 | print(reading) 26 | 27 | ``` 28 | 29 | 30 | ## Postman 31 | To use the API manually through Postman - read [Postman Collection Guide](POSTMAN.md) -------------------------------------------------------------------------------- /README_DEV.md: -------------------------------------------------------------------------------- 1 | # iec-api - Developer information 2 | 3 | A python wrapper for Israel Electric Company API 4 | 5 | ## Technology and Resources 6 | 7 | - [Python 3.10](https://www.python.org/downloads/release/python-3100/) - **pre-requisite** 8 | - [Docker](https://www.docker.com/get-started) - **pre-requisite** 9 | - [Docker Compose](https://docs.docker.com/compose/) - **pre-requisite** 10 | - [Poetry](https://python-poetry.org/) - **pre-requisite** 11 | - [Ruff](https://github.com/astral-sh/ruff) 12 | - [Dynaconf](https://www.dynaconf.com/) 13 | 14 | *Please pay attention on **pre-requisites** resources that you must install/configure.* 15 | 16 | ## How to install, run and test 17 | 18 | ### Environment variables 19 | 20 | *Use this section to explain each env variable available on your application* 21 | 22 | Variable | Description | Available Values | Default Value | Required 23 | --- | --- | --- | --- | --- 24 | ENV | The application enviroment | `development / test / qa / prod` | `development` | Yes 25 | 26 | *Note: When you run the install command (using docker or locally), a .env file will be created automatically based on [env.template](env.template)* 27 | 28 | Command | Docker | Locally | Description 29 | ---- | ------- | ------- | ------- 30 | install | `make docker/install` | `make local/install` | to install 31 | tests | `make docker/tests` | `make local/tests` | to run the tests with coverage 32 | lint | `make docker/lint` | `make local/lint` | to run static code analysis using ruff 33 | lint/fix | `make docker/lint/fix` | `make local/lint/fix` | to fix files using ruff 34 | run | `make docker/run` | `make local/run` | to run the project 35 | 36 | **Helpful commands** 37 | 38 | *Please, check all available commands in the [Makefile](Makefile) for more information*. 39 | 40 | ## Logging 41 | 42 | This project uses a simple way to configure the log with [logging.conf](logging.conf) to show the logs on the container output console. 43 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.4" 2 | services: 3 | iec-api: 4 | tty: true 5 | image: "iec-api" 6 | stdin_open: true 7 | build: 8 | context: . 9 | target: "development" 10 | volumes: 11 | - ".:/iec_api" 12 | # env_file: .env -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """ Main IEC Python API module. """ 2 | 3 | import asyncio 4 | import concurrent.futures 5 | import logging 6 | import os 7 | from datetime import datetime, timedelta 8 | 9 | import aiohttp 10 | 11 | from iec_api.iec_client import IecClient 12 | from iec_api.login import IECLoginError 13 | from iec_api.models.exceptions import IECError 14 | from iec_api.usage_calculator.calculator import UsageCalculator 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | async def main(): 20 | logging.basicConfig(level=logging.DEBUG) 21 | 22 | session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False), timeout=aiohttp.ClientTimeout(total=10)) 23 | try: 24 | # Example of usage 25 | client = IecClient(123456782, session) 26 | 27 | token_json_file = "token.json" 28 | if os.path.exists(token_json_file): 29 | await client.load_token_from_file(token_json_file) 30 | else: 31 | try: 32 | await client.login_with_id() 33 | with concurrent.futures.ThreadPoolExecutor() as pool: 34 | otp = await asyncio.get_event_loop().run_in_executor(pool, input, "Enter the OTP received: ") 35 | await client.verify_otp(otp) 36 | await client.save_token_to_file(token_json_file) 37 | except IECLoginError as err: 38 | logger.error(f"Failed Login: (Code {err.code}): {err.error}") 39 | raise 40 | 41 | # refresh token example 42 | token = client.get_token() 43 | await client.check_token() 44 | new_token = client.get_token() 45 | if token != new_token: 46 | print("Token refreshed") 47 | await client.save_token_to_file(token_json_file) 48 | 49 | print("id_token: " + token.id_token) 50 | 51 | tariff = await client.get_kwh_tariff() 52 | print(tariff) 53 | 54 | # client.manual_login() 55 | customer = await client.get_customer() 56 | print(customer) 57 | 58 | contracts = await client.get_contracts() 59 | for contract in contracts: 60 | print(contract) 61 | 62 | reading = await client.get_last_meter_reading(customer.bp_number, contracts[0].contract_id) 63 | print(reading) 64 | 65 | devices = await client.get_devices() 66 | device = devices[0] 67 | print(device) 68 | 69 | device_details = await client.get_device_by_device_id(device.device_number) 70 | print(device_details) 71 | 72 | # Get Remote Readings from the last three days 73 | 74 | selected_date: datetime = datetime.now() - timedelta(days=30) 75 | 76 | remote_readings = await client.get_remote_reading( 77 | device.device_number, int(device.device_code), selected_date, selected_date 78 | ) 79 | 80 | if remote_readings: 81 | print("Got " + str(len(remote_readings.data)) + " readings for " + selected_date.strftime("%Y-%m-%d")) 82 | for remote_reading in remote_readings.data: 83 | print(remote_reading.date, remote_reading.value) 84 | else: 85 | print("Got no readings") 86 | 87 | print(await client.get_electric_bill()) 88 | print(await client.get_device_type()) 89 | print(await client.get_billing_invoices()) 90 | except IECError as err: 91 | logger.error(f"IEC Error: (Code {err.code}): {err.error}") 92 | finally: 93 | await session.close() 94 | 95 | # 96 | # Example of usage of UsageCalculator 97 | # 98 | session = aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False), timeout=aiohttp.ClientTimeout(total=10)) 99 | try: 100 | usage_calculator = UsageCalculator() 101 | await usage_calculator.load_data(session) 102 | 103 | # Get kWh Tariff 104 | tariff = usage_calculator.get_kwh_tariff() 105 | print(f"kWh Tariff: {tariff} ILS/kWh") 106 | 107 | # Get all device names 108 | device_names = usage_calculator.get_device_names() 109 | print(device_names) 110 | 111 | # Select "Air-conditioner" 112 | device_name = device_names[8] 113 | print(f"Selected device: [{device_name}]") 114 | 115 | # Get device info by name 116 | device = usage_calculator.get_device_info_by_name(device_name) 117 | print(device) 118 | 119 | # Get default utility consumption by time 120 | consumption = usage_calculator.get_consumption_by_device_and_time(device_name, timedelta(days=1), None) 121 | print(consumption) 122 | 123 | # You can specify specific power usage of your device: 124 | # e.g. 3.5HP air-conditioner running for 6 hours 125 | consumption = usage_calculator.get_consumption_by_device_and_time( 126 | device_name, timedelta(hours=6), custom_usage_value=3.5 127 | ) 128 | print( 129 | f"Running a {consumption.power} {consumption.power_unit.name} {consumption.name} " 130 | f"for {consumption.duration.seconds // (60 * 60)} hours would cost: " 131 | f"{round(consumption.cost, 2)} ILS" 132 | ) 133 | 134 | finally: 135 | await session.close() 136 | 137 | 138 | if __name__ == "__main__": # pragma: no cover 139 | asyncio.run(main()) 140 | -------------------------------------------------------------------------------- /iec_api/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py for the iec_api package 2 | -------------------------------------------------------------------------------- /iec_api/commons.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import http 3 | import json 4 | import logging 5 | import re 6 | from concurrent.futures import ThreadPoolExecutor 7 | from datetime import datetime 8 | from json import JSONDecodeError 9 | from typing import Any, Optional 10 | 11 | import aiohttp 12 | import pytz 13 | from aiohttp import ClientError, ClientResponse, ClientSession, StreamReader 14 | 15 | from iec_api.const import ERROR_FIELD_NAME, ERROR_SUMMARY_FIELD_NAME, TIMEZONE 16 | from iec_api.models.error_response import IecErrorResponse 17 | from iec_api.models.exceptions import IECError, IECLoginError 18 | from iec_api.models.okta_errors import OktaError 19 | from iec_api.models.response_descriptor import RESPONSE_DESCRIPTOR_FIELD, ErrorResponseDescriptor 20 | 21 | logger = logging.getLogger(__name__) 22 | 23 | 24 | def add_auth_bearer_to_headers(headers: dict[str, str], token: str) -> dict[str, str]: 25 | """ 26 | Add JWT bearer token to the Authorization header. 27 | Args: 28 | headers (dict): The headers dictionary to be modified. 29 | token (str): The JWT token to be added to the headers. 30 | Returns: 31 | dict: The modified headers dictionary with the JWT token added. 32 | """ 33 | headers["Authorization"] = f"Bearer {token}" 34 | return headers 35 | 36 | 37 | PHONE_REGEX = "^(+972|0)5[0-9]{8}$" 38 | 39 | 40 | def check_phone(phone: str): 41 | """ 42 | Check if the phone number is valid. 43 | Args: 44 | phone (str): The phone number to be checked. 45 | Returns: 46 | bool: True if the phone number is valid, False otherwise. 47 | """ 48 | if not phone or not re.match(PHONE_REGEX, phone): 49 | raise ValueError("Invalid phone number") 50 | 51 | 52 | def is_valid_israeli_id(id_number: str | int) -> bool: 53 | """ 54 | Check if the ID number is valid. 55 | Args: 56 | id_number (str): The ID number to be checked. 57 | Returns: 58 | bool: True if the ID number is valid, False otherwise. 59 | """ 60 | 61 | id_str = str(id_number).strip() 62 | if len(id_str) > 9 or not id_str.isdigit(): 63 | return False 64 | id_str = id_str.zfill(9) 65 | return ( 66 | sum( 67 | (int(digit) if i % 2 == 0 else int(digit) * 2 if int(digit) * 2 < 10 else int(digit) * 2 - 9) 68 | for i, digit in enumerate(id_str) 69 | ) 70 | % 10 71 | == 0 72 | ) 73 | 74 | 75 | def is_json(s: str): 76 | try: 77 | json.loads(s) 78 | except ValueError: 79 | return False 80 | return True 81 | 82 | 83 | async def read_user_input(prompt: str) -> str: 84 | with ThreadPoolExecutor(1, "AsyncInput") as executor: 85 | return await asyncio.get_event_loop().run_in_executor(executor, input, prompt) 86 | 87 | 88 | def parse_error_response(resp: ClientResponse, json_resp: dict[str, Any]): 89 | """ 90 | A function to parse error responses from IEC or Okta Server 91 | """ 92 | logger.warning(f"Failed call: (Code {resp.status}): {resp.reason}") 93 | if json_resp and len(json_resp) > 0: 94 | if json_resp.get(RESPONSE_DESCRIPTOR_FIELD) is not None: 95 | login_error_response = ErrorResponseDescriptor.from_dict(json_resp.get(RESPONSE_DESCRIPTOR_FIELD)) 96 | raise IECError(login_error_response.code, login_error_response.error) 97 | elif json_resp.get(ERROR_FIELD_NAME) is not None: 98 | error_response = IecErrorResponse.from_dict(json_resp) 99 | raise IECError(error_response.code, error_response.error) 100 | elif json_resp.get(ERROR_SUMMARY_FIELD_NAME) is not None: 101 | login_error_response = OktaError.from_dict(json_resp) 102 | raise IECLoginError(resp.status, resp.reason + ": " + login_error_response.error_summary) 103 | raise IECError(resp.status, resp.reason) 104 | 105 | 106 | async def send_get_request( 107 | session: ClientSession, url: str, timeout: Optional[int] = 60, headers: Optional[dict] = None 108 | ) -> dict[str, Any]: 109 | try: 110 | if not headers: 111 | headers = session.headers 112 | 113 | if not timeout: 114 | timeout = session.timeout 115 | 116 | resp = await session.get(url=url, headers=headers, timeout=timeout) 117 | json_resp: dict = await resp.json(content_type=None) 118 | except TimeoutError as ex: 119 | raise IECError(-1, f"Failed to communicate with IEC API due to time out: ({str(ex)})") 120 | except ClientError as ex: 121 | raise IECError(-1, f"Failed to communicate with IEC API due to ClientError: ({str(ex)})") 122 | except JSONDecodeError as ex: 123 | raise IECError(-1, f"Received invalid response from IEC API: {str(ex)}") 124 | 125 | if resp.status != http.HTTPStatus.OK: 126 | parse_error_response(resp, json_resp) 127 | 128 | return json_resp 129 | 130 | 131 | async def send_non_json_get_request( 132 | session: ClientSession, 133 | url: str, 134 | timeout: Optional[int] = 60, 135 | headers: Optional[dict] = None, 136 | encoding: Optional[str] = None, 137 | ) -> str: 138 | try: 139 | if not headers: 140 | headers = session.headers 141 | 142 | if not timeout: 143 | timeout = session.timeout 144 | 145 | resp = await session.get(url=url, headers=headers, timeout=timeout) 146 | resp_content = await resp.text(encoding=encoding) 147 | except TimeoutError as ex: 148 | raise IECError(-1, f"Failed to communicate with IEC API due to time out: ({str(ex)})") 149 | except ClientError as ex: 150 | raise IECError(-1, f"Failed to communicate with IEC API due to ClientError: ({str(ex)})") 151 | except JSONDecodeError as ex: 152 | raise IECError(-1, f"Received invalid response from IEC API: {str(ex)}") 153 | 154 | return resp_content 155 | 156 | 157 | async def send_post_request( 158 | session: ClientSession, 159 | url: str, 160 | timeout: Optional[int] = 60, 161 | headers: Optional[dict] = None, 162 | data: Optional[dict] = None, 163 | json_data: Optional[dict] = None, 164 | ) -> dict[str, Any]: 165 | try: 166 | if not headers: 167 | headers = session.headers 168 | 169 | if not timeout: 170 | timeout = session.timeout 171 | 172 | resp = await session.post(url=url, data=data, json=json_data, headers=headers, timeout=timeout) 173 | 174 | json_resp: dict = await resp.json(content_type=None) 175 | except TimeoutError as ex: 176 | raise IECError(-1, f"Failed to communicate with IEC API due to time out: ({str(ex)})") 177 | except ClientError as ex: 178 | raise IECError(-1, f"Failed to communicate with IEC API due to ClientError: ({str(ex)})") 179 | except JSONDecodeError as ex: 180 | raise IECError(-1, f"Received invalid response from IEC API: {str(ex)}") 181 | 182 | if resp.status != http.HTTPStatus.OK: 183 | parse_error_response(resp, json_resp) 184 | return json_resp 185 | 186 | 187 | async def send_non_json_post_request( 188 | session: ClientSession, 189 | url: str, 190 | timeout: Optional[int] = 60, 191 | headers: Optional[dict] = None, 192 | data: Optional[dict] = None, 193 | json_data: Optional[dict] = None, 194 | ) -> StreamReader: 195 | try: 196 | if not headers: 197 | headers = session.headers 198 | 199 | if not timeout: 200 | headers = session.timeout 201 | 202 | resp = await session.post(url=url, data=data, json=json_data, headers=headers, timeout=timeout) 203 | except TimeoutError as ex: 204 | raise IECError(-1, f"Failed to communicate with IEC API due to time out: ({str(ex)})") 205 | except ClientError as ex: 206 | raise IECError(-1, f"Failed to communicate with IEC API due to ClientError: ({str(ex)})") 207 | except JSONDecodeError as ex: 208 | raise IECError(-1, f"Received invalid response from IEC API: {str(ex)}") 209 | 210 | if resp.status != http.HTTPStatus.OK: 211 | if is_json(resp): 212 | parse_error_response(resp, json.loads(resp.content)) 213 | raise IECError(resp.status, resp.reason) 214 | return resp.content 215 | 216 | 217 | def convert_to_tz_aware_datetime(dt: Optional[datetime]) -> Optional[datetime]: 218 | """ 219 | Convert a datetime object to a timezone aware datetime object. 220 | Args: 221 | dt (Optional[datetime]): The datetime object to be converted. 222 | Returns: 223 | Optional[datetime]: The timezone aware datetime object, or None if dt is None. 224 | """ 225 | if dt is None: 226 | return None 227 | elif dt.year > 2000: # Fix '0001-01-01T00:00:00' values 228 | return TIMEZONE.localize(dt) 229 | else: 230 | return dt.replace(tzinfo=pytz.utc) 231 | 232 | 233 | async def on_request_start_debug(session: aiohttp.ClientSession, context, params: aiohttp.TraceRequestStartParams): 234 | logger.debug(f"HTTP {params.method}: {params.url}") 235 | 236 | 237 | async def on_request_chunk_sent_debug( 238 | session: aiohttp.ClientSession, context, params: aiohttp.TraceRequestChunkSentParams 239 | ): 240 | if (params.method == "POST" or params.method == "PUT") and params.chunk: 241 | logger.debug(f"HTTP Content {params.method}: {params.chunk}") 242 | 243 | 244 | async def on_request_end_debug(session: aiohttp.ClientSession, context, params: aiohttp.TraceRequestEndParams): 245 | logger.debug(f"HTTP {params.method} call from {params.url} - Response <{params.response.status}>: \ 246 | {await params.response.text()}") 247 | -------------------------------------------------------------------------------- /iec_api/const.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | 3 | HEADERS_NO_AUTH = { 4 | "authority": "iecapi.iec.co.il", 5 | "accept": "application/json, text/plain, */*", 6 | "accept-language": "en,he;q=0.9", 7 | "dnt": "1", 8 | "origin": "https://www.iec.co.il", 9 | "referer": "https://www.iec.co.il/", 10 | "sec-ch-ua": '"Chromium";v="121", "Not A(Brand";v="99"', 11 | "sec-ch-ua-mobile": "?0", 12 | "sec-ch-ua-platform": '"macOS"', 13 | "sec-fetch-dest": "empty", 14 | "sec-fetch-mode": "cors", 15 | "sec-fetch-site": "same-site", 16 | "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) " 17 | "Chrome/121.0.0.0 Safari/537.36", 18 | "x-iec-idt": "1", 19 | "x-iec-webview": "1", 20 | } 21 | 22 | HEADERS_WITH_AUTH = HEADERS_NO_AUTH.copy() # Make a copy of the original dictionary 23 | HEADERS_WITH_AUTH["Authorization"] = "Bearer 1234" 24 | HEADERS_WITH_AUTH["Cookie"] = "ARRAffinity=?; " "ARRAffinitySameSite=?;" " GCLB=?" 25 | 26 | TIMEZONE = pytz.timezone("Asia/Jerusalem") 27 | IEC_API_BASE_URL = "https://iecapi.iec.co.il/api/" 28 | IEC_FAULT_PORTAL_API_URL = "https://masa-faultsportalapi.iec.co.il/api/" 29 | IEC_MASA_BASE_URL = "https://masaapi-wa.azurewebsites.net/" 30 | IEC_MASA_API_BASE_URL = IEC_MASA_BASE_URL + "api/" 31 | IEC_MASA_LOOKUP_BASE_URL = IEC_MASA_BASE_URL + "lookup/" 32 | IEC_MASA_MAINPORTAL_API_BASE_URL = "https://masa-mainportalapi.iec.co.il/api/" 33 | 34 | GET_ACCOUNTS_URL = IEC_API_BASE_URL + "outages/accounts" 35 | GET_CONSUMER_URL = IEC_API_BASE_URL + "customer" 36 | GET_REQUEST_READING_URL = IEC_API_BASE_URL + "Consumption/RemoteReadingRange/{contract_id}" 37 | GET_ELECTRIC_BILL_URL = IEC_API_BASE_URL + "ElectricBillsDrawers/ElectricBills/{contract_id}/{bp_number}" 38 | GET_CONTRACTS_URL = IEC_API_BASE_URL + "customer/contract/{bp_number}" 39 | GET_CHECK_CONTRACT_URL = IEC_API_BASE_URL + "customer/checkContract/{{contract_id}}/6" 40 | GET_EFS_MESSAGES_URL = IEC_API_BASE_URL + "customer/efs" 41 | GET_DEFAULT_CONTRACT_URL = GET_CONTRACTS_URL + "?count=1" 42 | GET_LAST_METER_READING_URL = IEC_API_BASE_URL + "Device/LastMeterReading/{contract_id}/{bp_number}" 43 | AUTHENTICATE_URL = IEC_API_BASE_URL + "Authentication/{id}/1/-1?customErrorPage=true" 44 | GET_DEVICES_URL = IEC_API_BASE_URL + "Device/{contract_id}" 45 | GET_TENANT_IDENTITY_URL = IEC_API_BASE_URL + "Tenant/Identify/{device_id}" 46 | GET_DEVICE_BY_DEVICE_ID_URL = GET_DEVICES_URL + "/{device_id}" 47 | GET_DEVICE_TYPE_URL = IEC_API_BASE_URL + "Device/type/{bp_number}/{contract_id}/false" 48 | GET_BILLING_INVOICES_URL = IEC_API_BASE_URL + "BillingCollection/invoices/{contract_id}/{bp_number}" 49 | GET_INVOICE_PDF_URL = IEC_API_BASE_URL + "BillingCollection/pdf" 50 | GET_KWH_TARIFF_URL = IEC_API_BASE_URL + "content/he-IL/content/tariffs/contentpages/homeelectricitytariff" 51 | GET_PREIOD_CALCULATOR_URL = IEC_API_BASE_URL + "content/he-IL/calculators/period" 52 | GET_CALCULATOR_GADGET_URL = IEC_API_BASE_URL + "content/he-IL/calculators/gadget" 53 | GET_OUTAGES_URL = IEC_API_BASE_URL + "outages/transactions/{account_id}/2" 54 | SEND_CONSUMPTION_REPORT_TO_MAIL_URL = IEC_API_BASE_URL + "/Consumption/SendConsumptionReportToMail/{contract_id}" 55 | 56 | GET_MASA_CITIES_LOOKUP_URL = IEC_MASA_MAINPORTAL_API_BASE_URL + "cities" 57 | GET_MASA_USER_PROFILE_LOOKUP_URL = IEC_MASA_MAINPORTAL_API_BASE_URL + "contacts/userprofile" 58 | GET_MASA_ORDER_TITLES_URL = IEC_MASA_API_BASE_URL + "accounts/{account_id}/orders/titles" 59 | GET_MASA_ORDER_LOOKUP_URL = IEC_MASA_API_BASE_URL + "orderLookup" 60 | GET_MASA_VOLT_LEVELS_URL = IEC_MASA_API_BASE_URL + "voltLevels/active" 61 | GET_MASA_EQUIPMENTS_URL = IEC_MASA_BASE_URL + "equipments/get?accountId={account_id}&pageNumber=1&pageSize=10" 62 | GET_MASA_LOOKUP_URL = IEC_MASA_BASE_URL + "lookup/all" 63 | 64 | GET_USER_PROFILE_FROM_FAULT_PORTAL_URL = IEC_FAULT_PORTAL_API_URL + "contacts/userprofile" 65 | GET_OUTAGES_FROM_FAULT_PORTAL_URL = IEC_FAULT_PORTAL_API_URL + "accounts/{account_id}/tranzactions/2" 66 | ERROR_FIELD_NAME = "Error" 67 | ERROR_SUMMARY_FIELD_NAME = "errorSummary" 68 | -------------------------------------------------------------------------------- /iec_api/data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | from typing import List, Optional, TypeVar 4 | 5 | from aiohttp import ClientSession 6 | from mashumaro.codecs import BasicDecoder 7 | 8 | from iec_api import commons 9 | from iec_api.const import ( 10 | GET_ACCOUNTS_URL, 11 | GET_BILLING_INVOICES_URL, 12 | GET_CHECK_CONTRACT_URL, 13 | GET_CONSUMER_URL, 14 | GET_CONTRACTS_URL, 15 | GET_DEFAULT_CONTRACT_URL, 16 | GET_DEVICE_BY_DEVICE_ID_URL, 17 | GET_DEVICE_TYPE_URL, 18 | GET_DEVICES_URL, 19 | GET_EFS_MESSAGES_URL, 20 | GET_ELECTRIC_BILL_URL, 21 | GET_INVOICE_PDF_URL, 22 | GET_LAST_METER_READING_URL, 23 | GET_OUTAGES_URL, 24 | GET_REQUEST_READING_URL, 25 | GET_TENANT_IDENTITY_URL, 26 | HEADERS_WITH_AUTH, 27 | SEND_CONSUMPTION_REPORT_TO_MAIL_URL, 28 | ) 29 | from iec_api.models.account import Account 30 | from iec_api.models.account import decoder as account_decoder 31 | from iec_api.models.contract import Contract, Contracts 32 | from iec_api.models.contract import decoder as contract_decoder 33 | from iec_api.models.contract_check import ContractCheck 34 | from iec_api.models.contract_check import decoder as contract_check_decoder 35 | from iec_api.models.customer import Customer 36 | from iec_api.models.device import Device, Devices 37 | from iec_api.models.device import decoder as devices_decoder 38 | from iec_api.models.device_identity import DeviceDetails, DeviceIdentity 39 | from iec_api.models.device_identity import decoder as device_identity_decoder 40 | from iec_api.models.device_type import DeviceType 41 | from iec_api.models.device_type import decoder as device_type_decoder 42 | from iec_api.models.efs import EfsMessage, EfsRequestAllServices, EfsRequestSingleService 43 | from iec_api.models.efs import decoder as efs_decoder 44 | from iec_api.models.electric_bill import ElectricBill 45 | from iec_api.models.electric_bill import decoder as electric_bill_decoder 46 | from iec_api.models.exceptions import IECError 47 | from iec_api.models.get_pdf import GetPdfRequest 48 | from iec_api.models.invoice import GetInvoicesBody 49 | from iec_api.models.invoice import decoder as invoice_decoder 50 | from iec_api.models.jwt import JWT 51 | from iec_api.models.meter_reading import MeterReadings 52 | from iec_api.models.meter_reading import decoder as meter_reading_decoder 53 | from iec_api.models.outages import Outage 54 | from iec_api.models.outages import decoder as outages_decoder 55 | from iec_api.models.remote_reading import ReadingResolution, RemoteReadingRequest, RemoteReadingResponse 56 | from iec_api.models.response_descriptor import ResponseWithDescriptor 57 | from iec_api.models.send_consumption_to_mail import SendConsumptionReportToMailRequest 58 | 59 | T = TypeVar("T") 60 | logger = logging.getLogger(__name__) 61 | 62 | 63 | async def _get_response_with_descriptor( 64 | session: ClientSession, jwt_token: JWT, request_url: str, decoder: BasicDecoder[ResponseWithDescriptor[T]] 65 | ) -> T: 66 | """ 67 | A function to retrieve a response with a descriptor using a JWT token and a URL. 68 | 69 | Args: 70 | jwt_token (JWT): The JWT token used for authentication. 71 | request_url (str): The URL to send the request to. 72 | 73 | Returns: 74 | T: The response with a descriptor, with its type specified by the return type annotation. 75 | """ 76 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, jwt_token.id_token) 77 | response = await commons.send_get_request(session=session, url=request_url, headers=headers) 78 | 79 | response_with_descriptor = decoder.decode(response) 80 | 81 | if not response_with_descriptor.data and not response_with_descriptor.response_descriptor.is_success: 82 | raise IECError( 83 | response_with_descriptor.response_descriptor.code, response_with_descriptor.response_descriptor.description 84 | ) 85 | 86 | return response_with_descriptor.data 87 | 88 | 89 | async def _post_response_with_descriptor( 90 | session: ClientSession, 91 | jwt_token: JWT, 92 | request_url: str, 93 | json_data: Optional[dict], 94 | decoder: BasicDecoder[ResponseWithDescriptor[T]], 95 | ) -> T: 96 | """ 97 | A function to retrieve a response with a descriptor using a JWT token and a URL. 98 | 99 | Args: 100 | jwt_token (JWT): The JWT token used for authentication. 101 | request_url (str): The URL to send the request to. 102 | json_data (dict): POST content 103 | 104 | Returns: 105 | T: The response with a descriptor, with its type specified by the return type annotation. 106 | """ 107 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, jwt_token.id_token) 108 | response = await commons.send_post_request(session=session, url=request_url, headers=headers, json_data=json_data) 109 | 110 | response_with_descriptor = decoder.decode(response) 111 | 112 | if not response_with_descriptor.data and not response_with_descriptor.response_descriptor.is_success: 113 | raise IECError( 114 | response_with_descriptor.response_descriptor.code, response_with_descriptor.response_descriptor.description 115 | ) 116 | 117 | return response_with_descriptor.data 118 | 119 | 120 | async def get_accounts(session: ClientSession, token: JWT) -> Optional[List[Account]]: 121 | """Get Accounts response from IEC API.""" 122 | return await _get_response_with_descriptor(session, token, GET_ACCOUNTS_URL, account_decoder) 123 | 124 | 125 | async def get_customer(session: ClientSession, token: JWT) -> Optional[Customer]: 126 | """Get customer data response from IEC API.""" 127 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 128 | # sending get request and saving the response as response object 129 | response = await commons.send_get_request(session=session, url=GET_CONSUMER_URL, headers=headers) 130 | 131 | return Customer.from_dict(response) 132 | 133 | 134 | async def get_remote_reading( 135 | session: ClientSession, 136 | token: JWT, 137 | contract_id: str, 138 | meter_serial_number: str, 139 | meter_code: int, 140 | last_invoice_date: datetime, 141 | from_date: datetime, 142 | resolution: ReadingResolution = ReadingResolution.DAILY, 143 | ) -> Optional[RemoteReadingResponse]: 144 | req = RemoteReadingRequest( 145 | meter_serial_number=meter_serial_number, 146 | meter_code=str(meter_code), 147 | last_invoice_date=last_invoice_date.strftime("%Y-%m-%d"), 148 | from_date=from_date.strftime("%Y-%m-%d"), 149 | resolution=resolution, 150 | ) 151 | 152 | url = GET_REQUEST_READING_URL.format(contract_id=contract_id) 153 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 154 | 155 | response = await commons.send_post_request(session=session, url=url, headers=headers, json_data=req.to_dict()) 156 | 157 | return RemoteReadingResponse.from_dict(response) 158 | 159 | 160 | async def get_efs_messages( 161 | session: ClientSession, token: JWT, contract_id: str, service_code: Optional[int] = None 162 | ) -> Optional[List[EfsMessage]]: 163 | """Get EFS Messages response from IEC API.""" 164 | if service_code: 165 | req = EfsRequestSingleService(contract_number=contract_id, process_type=1, service_code=f"EFS{service_code:03}") 166 | else: 167 | req = EfsRequestAllServices(contract_number=contract_id, process_type=1) 168 | 169 | url = GET_EFS_MESSAGES_URL 170 | 171 | return await _post_response_with_descriptor(session, token, url, json_data=req.to_dict(), decoder=efs_decoder) 172 | 173 | 174 | async def get_electric_bill( 175 | session: ClientSession, token: JWT, bp_number: str, contract_id: str 176 | ) -> Optional[ElectricBill]: 177 | """Get Electric Bill data response from IEC API.""" 178 | return await _get_response_with_descriptor( 179 | session, 180 | token, 181 | GET_ELECTRIC_BILL_URL.format(contract_id=contract_id, bp_number=bp_number), 182 | electric_bill_decoder, 183 | ) 184 | 185 | 186 | async def get_default_contract(session: ClientSession, token: JWT, bp_number: str) -> Optional[Contract]: 187 | """Get Contract data response from IEC API.""" 188 | return await _get_response_with_descriptor( 189 | session, token, GET_DEFAULT_CONTRACT_URL.format(bp_number=bp_number), contract_decoder 190 | ) 191 | 192 | 193 | async def get_contracts(session: ClientSession, token: JWT, bp_number: str) -> Optional[Contracts]: 194 | """Get all user's Contracts from IEC API.""" 195 | return await _get_response_with_descriptor( 196 | session, token, GET_CONTRACTS_URL.format(bp_number=bp_number), contract_decoder 197 | ) 198 | 199 | 200 | async def get_contract_check(session: ClientSession, token: JWT, contract_id: str) -> Optional[ContractCheck]: 201 | """Get Contract Check response from IEC API.""" 202 | return await _get_response_with_descriptor( 203 | session, token, GET_CHECK_CONTRACT_URL.format(contract_id=contract_id), contract_check_decoder 204 | ) 205 | 206 | 207 | async def get_last_meter_reading( 208 | session: ClientSession, token: JWT, bp_number: str, contract_id: str 209 | ) -> Optional[MeterReadings]: 210 | """Get Last Meter Reading data response from IEC API.""" 211 | return await _get_response_with_descriptor( 212 | session, 213 | token, 214 | GET_LAST_METER_READING_URL.format(contract_id=contract_id, bp_number=bp_number), 215 | meter_reading_decoder, 216 | ) 217 | 218 | 219 | async def get_devices(session: ClientSession, token: JWT, contract_id: str) -> list[Device]: 220 | """Get Device data response from IEC API.""" 221 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 222 | # sending get request and saving the response as response object 223 | response = await commons.send_get_request( 224 | session=session, url=GET_DEVICES_URL.format(contract_id=contract_id), headers=headers 225 | ) 226 | 227 | return [Device.from_dict(device) for device in response] 228 | 229 | 230 | async def get_device_details(session: ClientSession, token: JWT, device_id: str) -> Optional[List[DeviceDetails]]: 231 | """Get Device Details response from IEC API.""" 232 | device_identity: DeviceIdentity = await _get_response_with_descriptor( 233 | session, token, GET_TENANT_IDENTITY_URL.format(device_id=device_id), device_identity_decoder 234 | ) 235 | 236 | return device_identity.device_details if device_identity else None 237 | 238 | 239 | async def get_device_details_by_code( 240 | session: ClientSession, token: JWT, device_id: str, device_code: str 241 | ) -> Optional[DeviceDetails]: 242 | """Get Device Details response from IEC API.""" 243 | devices = await get_device_details(session, token, device_id) 244 | 245 | return next((device for device in devices if device.device_code == device_code), None) 246 | 247 | 248 | async def get_device_by_device_id( 249 | session: ClientSession, token: JWT, contract_id: str, device_id: str 250 | ) -> Optional[Devices]: 251 | """Get Device data response from IEC API.""" 252 | return await _get_response_with_descriptor( 253 | session, 254 | token, 255 | GET_DEVICE_BY_DEVICE_ID_URL.format(device_id=device_id, contract_id=contract_id), 256 | devices_decoder, 257 | ) 258 | 259 | 260 | async def get_device_type(session: ClientSession, token: JWT, bp_number: str, contract_id: str) -> Optional[DeviceType]: 261 | """Get Device Type data response from IEC API.""" 262 | # sending get request and saving the response as response object 263 | return await _get_response_with_descriptor( 264 | session, token, GET_DEVICE_TYPE_URL.format(bp_number=bp_number, contract_id=contract_id), device_type_decoder 265 | ) 266 | 267 | 268 | async def get_billing_invoices( 269 | session: ClientSession, token: JWT, bp_number: str, contract_id: str 270 | ) -> Optional[GetInvoicesBody]: 271 | """Get Device Type data response from IEC API.""" 272 | return await _get_response_with_descriptor( 273 | session, token, GET_BILLING_INVOICES_URL.format(bp_number=bp_number, contract_id=contract_id), invoice_decoder 274 | ) 275 | 276 | 277 | async def get_invoice_pdf( 278 | session: ClientSession, token: JWT, bp_number: int | str, contract_id: int | str, invoice_number: int | str 279 | ) -> bytes: 280 | """Get Invoice PDF response from IEC API.""" 281 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 282 | headers = headers.copy() # don't modify original headers 283 | headers.update({"accept": "application/pdf", "content-type": "application/json"}) 284 | 285 | request = GetPdfRequest( 286 | invoice_number=str(invoice_number), contract_id=str(contract_id), bp_number=str(bp_number) 287 | ).to_dict() 288 | response = await commons.send_non_json_post_request( 289 | session, url=GET_INVOICE_PDF_URL, headers=headers, json_data=request 290 | ) 291 | return await response.read() 292 | 293 | 294 | async def send_consumption_report_to_mail( 295 | session: ClientSession, token: JWT, contract_id: int | str, email: str, device_code: int | str, device_id: int | str 296 | ) -> bool: 297 | """Send Consumption Report to Mail from IEC API.""" 298 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 299 | 300 | request = SendConsumptionReportToMailRequest( 301 | email=email, device_code=str(device_code), device_id=str(device_id) 302 | ).to_dict() 303 | response = await commons.send_non_json_post_request( 304 | session, 305 | url=SEND_CONSUMPTION_REPORT_TO_MAIL_URL.format(contract_id=contract_id), 306 | headers=headers, 307 | json_data=request, 308 | ) 309 | return await bool(response.read()) 310 | 311 | 312 | async def get_outages_by_account(session: ClientSession, token: JWT, account_id: str) -> Optional[list[Outage]]: 313 | """Get Device Type data response from IEC API.""" 314 | return await _get_response_with_descriptor( 315 | session, token, GET_OUTAGES_URL.format(account_id=account_id), outages_decoder 316 | ) 317 | -------------------------------------------------------------------------------- /iec_api/fault_portal_data.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from aiohttp import ClientSession 4 | 5 | from iec_api import commons 6 | from iec_api.const import GET_USER_PROFILE_FROM_FAULT_PORTAL_URL, HEADERS_WITH_AUTH 7 | from iec_api.fault_portal_models.outages import FaultPortalOutage, OutagesResponse 8 | from iec_api.fault_portal_models.user_profile import UserProfile 9 | from iec_api.models.jwt import JWT 10 | 11 | 12 | async def get_user_profile(session: ClientSession, token: JWT) -> Optional[UserProfile]: 13 | """Get User Profile from IEC Fault PortalAPI.""" 14 | 15 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 16 | # sending get request and saving the response as response object 17 | response = await commons.send_get_request( 18 | session=session, url=GET_USER_PROFILE_FROM_FAULT_PORTAL_URL, headers=headers 19 | ) 20 | 21 | return UserProfile.from_dict(response) 22 | 23 | 24 | async def get_outages_by_account( 25 | session: ClientSession, token: JWT, account_id: str 26 | ) -> Optional[List[FaultPortalOutage]]: 27 | """Get Outages from IEC Fault PortalAPI.""" 28 | 29 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 30 | # sending get request and saving the response as response object 31 | response = await commons.send_get_request( 32 | session=session, url=GET_USER_PROFILE_FROM_FAULT_PORTAL_URL, headers=headers 33 | ) 34 | 35 | return OutagesResponse.from_dict(response).data_collection 36 | -------------------------------------------------------------------------------- /iec_api/fault_portal_models/fault_portal_base.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | 8 | @dataclass 9 | class FaultPortalBase(DataClassDictMixin): 10 | """Base Class for Fault Portal Address""" 11 | 12 | id: Optional[UUID] = field(metadata=field_options(alias="id")) 13 | logical_name: Optional[str] = field(metadata=field_options(alias="logicalName")) 14 | -------------------------------------------------------------------------------- /iec_api/fault_portal_models/outages.py: -------------------------------------------------------------------------------- 1 | # Outages model 2 | 3 | from dataclasses import dataclass, field 4 | from datetime import datetime 5 | from typing import List, Optional 6 | 7 | from mashumaro import DataClassDictMixin, field_options 8 | 9 | from iec_api.fault_portal_models.fault_portal_base import FaultPortalBase 10 | 11 | # GET https://masa-faultsportalapi.iec.co.il/api/accounts/{account_id}}/tranzactions/2 12 | # 13 | # { 14 | # "dataCollection": 15 | # [ 16 | # { 17 | # "siteKeyDesc": "603147332", 18 | # "disconnectDate": "2024-05-17T22:32:16Z", 19 | # "disconnect": { 20 | # "disconnectKey": "17701HESD58403", 21 | # "disconnectTreatmentState": { 22 | # "displayName": "החזרת אספקה", 23 | # "code": "6", 24 | # "isConnectIndicationBit": false, 25 | # "disconnectTreatmentStatePortal": 3, 26 | # "id": "7eaecdd3-859a-ea11-a812-000d3a239136", 27 | # "logicalName": "iec_disconnecttreatmentstate" 28 | # }, 29 | # "disconnectType": { 30 | # "displayName": "תקלה איזורית", 31 | # "code": 1, 32 | # "id": "b146f469-819a-ea11-a811-000d3a239ca0", 33 | # "logicalName": "iec_disconnecttype" 34 | # }, 35 | # "energizedDate": "2024-05-18T11:09:41Z", 36 | # "disconnectDate": "2024-05-17T22:12:08Z", 37 | # "id": "1d9a36ef-9a14-ef11-9f89-7c1e52290237", 38 | # "logicalName": "iec_disconnect" 39 | # }, 40 | # "site": { 41 | # "contractNumber": "346496424", 42 | # "address": { 43 | # "area": { 44 | # "name": "חיפה", 45 | # "id": "0d93fbef-d7db-ea11-a813-000d3aabca53", 46 | # "logicalName": "iec_area" 47 | # }, 48 | # "region": { 49 | # "name": "חיפה והצפון", 50 | # "id": "909a5d57-d7db-ea11-a813-000d3aabca53", 51 | # "logicalName": "iec_region" 52 | # }, 53 | # "city": { 54 | # "name": "טירת כרמל", 55 | # "shovalCityCode": "778", 56 | # "id": "fb0a89b9-29e0-e911-a972-000d3a29fb7a", 57 | # "logicalName": "iec_city" 58 | # }, 59 | # "houseNumber": "11", 60 | # "streetStr": "הגל", 61 | # "id": "f5453a99-0472-e811-8106-3863bb358f68", 62 | # "logicalName": "iec_address" 63 | # }, 64 | # "id": "8eb5c7da-e0a5-ea11-a812-000d3aaebb51", 65 | # "logicalName": "iec_site" 66 | # }, 67 | # "id": "e95a1233-9b14-ef11-9f89-7c1e52290237", 68 | # "logicalName": "iec_tranzaction", 69 | # "stateCode": 1 70 | # } 71 | # ] 72 | # } 73 | # } 74 | 75 | 76 | @dataclass 77 | class FaultPortalAddress(FaultPortalBase): 78 | """Address Model for Area/Region/City""" 79 | 80 | name: Optional[str] = field(metadata=field_options(alias="name")) 81 | 82 | 83 | @dataclass 84 | class FaultPortalCity(FaultPortalAddress): 85 | """Address Model for Area/Region/City""" 86 | 87 | shoval_city_code: Optional[str] = field(metadata=field_options(alias="shovalCityCode")) 88 | 89 | 90 | @dataclass 91 | class FaultPortalFullAddress(FaultPortalBase): 92 | """Full Address Model""" 93 | 94 | area: Optional[FaultPortalAddress] = field(metadata=field_options(alias="area")) 95 | region: Optional[FaultPortalAddress] = field(metadata=field_options(alias="region")) 96 | city: Optional[FaultPortalCity] = field(metadata=field_options(alias="city")) 97 | house_number: Optional[str] = field(metadata=field_options(alias="houseNumber")) 98 | street_str: Optional[str] = field(metadata=field_options(alias="streetStr")) 99 | 100 | 101 | @dataclass 102 | class Site(FaultPortalBase): 103 | """Site Model""" 104 | 105 | contract_number: Optional[str] = field(metadata=field_options(alias="contractNumber")) 106 | address: Optional[FaultPortalFullAddress] = field(metadata=field_options(alias="address")) 107 | 108 | 109 | @dataclass 110 | class DisconnectTreatmentState(FaultPortalBase): 111 | """Disconnect Treatment State Model""" 112 | 113 | display_name: Optional[str] = field(metadata=field_options(alias="displayName")) 114 | code: Optional[int] = field(metadata=field_options(alias="code")) 115 | is_connect_indication_bit: Optional[bool] = field(metadata=field_options(alias="isConnectIndicationBit")) 116 | disconnect_treatment_state_portal: Optional[int] = field( 117 | metadata=field_options(alias="disconnectTreatmentStatePortal") 118 | ) 119 | 120 | 121 | @dataclass 122 | class DisconnectType(FaultPortalBase): 123 | """Disconnect Type Model""" 124 | 125 | display_name: Optional[str] = field(metadata=field_options(alias="displayName")) 126 | code: Optional[int] = field(metadata=field_options(alias="code")) 127 | 128 | 129 | @dataclass 130 | class Disconnect(FaultPortalBase): 131 | """Disconnect Model""" 132 | 133 | disconnect_key: Optional[str] = field(metadata=field_options(alias="disconnectKey")) 134 | disconnect_type: Optional[DisconnectType] = field(metadata=field_options(alias="disconnectType")) 135 | energized_date: Optional[datetime] = field(metadata=field_options(alias="energizedDate")) 136 | disconnect_date: Optional[datetime] = field(metadata=field_options(alias="disconnectDate")) 137 | disconnect_treatment_state: Optional[DisconnectTreatmentState] = field( 138 | metadata=field_options(alias="disconnectTreatmentState") 139 | ) 140 | 141 | 142 | @dataclass 143 | class FaultPortalOutage(FaultPortalBase): 144 | """Outage Model""" 145 | 146 | site_key_desc: Optional[str] = field(metadata=field_options(alias="siteKeyDesc")) 147 | disconnect_date: Optional[datetime] = field(metadata=field_options(alias="disconnectDate")) 148 | state_code: Optional[int] = field(metadata=field_options(alias="stateCode")) 149 | site: Optional[Site] = field(metadata=field_options(alias="site")) 150 | disconnect: Optional[Disconnect] = field(metadata=field_options(alias="disconnect")) 151 | 152 | 153 | @dataclass 154 | class OutagesResponse(DataClassDictMixin): 155 | data_collection: Optional[List[FaultPortalOutage]] = field(metadata=field_options(alias="dataCollection")) 156 | -------------------------------------------------------------------------------- /iec_api/fault_portal_models/user_profile.py: -------------------------------------------------------------------------------- 1 | # User Profile Model 2 | 3 | from dataclasses import dataclass, field 4 | from typing import Optional 5 | 6 | from mashumaro import field_options 7 | 8 | from iec_api.fault_portal_models.fault_portal_base import FaultPortalBase 9 | 10 | # GET https://masa-mainportalapi.iec.co.il/api/contacts/userprofile 11 | # 12 | # { 13 | # "governmentid": "123456", 14 | # "idType": 12345, 15 | # "email": "my@mail.com", 16 | # "phoneNumber": "1234567", 17 | # "accounts": [ 18 | # { 19 | # "name": "my_name", 20 | # "accountNumber": "123456", 21 | # "governmentNumber": "12345", 22 | # "accountType": 123456, 23 | # "viewTypeCode": 1, 24 | # "identificationType": 123456, 25 | # "consumptionOrderViewTypeCode": 1, 26 | # "id": "123456-1232-1312-1112-3563bb357f98", 27 | # "logicalName": "account" 28 | # } 29 | # ], 30 | # "phonePrefix": "050", 31 | # "isConnectedToPrivateAccount": false, 32 | # "isAccountOwner": false, 33 | # "isAccountContact": false, 34 | # "id": "123456-1232-1312-1112-3563bb357f98", 35 | # "logicalName": "contact" 36 | # } 37 | 38 | 39 | @dataclass 40 | class FaultPortalAccount(FaultPortalBase): 41 | name: Optional[str] = field(metadata=field_options(alias="name")) 42 | account_number: Optional[str] = field(metadata=field_options(alias="accountNumber")) 43 | government_number: Optional[str] = field(metadata=field_options(alias="governmentNumber")) 44 | account_type: Optional[int] = field(metadata=field_options(alias="accountType")) 45 | view_type_code: Optional[int] = field(metadata=field_options(alias="viewTypeCode")) 46 | identification_type: Optional[int] = field(metadata=field_options(alias="identificationType")) 47 | consumption_order_view_type_code: Optional[int] = field( 48 | metadata=field_options(alias="consumptionOrderViewTypeCode") 49 | ) 50 | 51 | 52 | @dataclass 53 | class UserProfile(FaultPortalBase): 54 | government_id: Optional[str] = field(metadata=field_options(alias="governmentId")) 55 | id_type: Optional[int] = field(metadata=field_options(alias="idType")) 56 | email: Optional[str] = field(metadata=field_options(alias="email")) 57 | phone_prefix: Optional[str] = field(metadata=field_options(alias="phonePrefix")) 58 | phone_number: Optional[str] = field(metadata=field_options(alias="phoneNumber")) 59 | accounts: Optional[list[FaultPortalAccount]] = field(metadata=field_options(alias="accounts")) 60 | is_connected_to_private_account: Optional[bool] = field(metadata=field_options(alias="isConnectedToPrivateAccount")) 61 | is_account_owner: Optional[bool] = field(metadata=field_options(alias="isAccountOwner")) 62 | is_account_contact: Optional[bool] = field(metadata=field_options(alias="isAccountContact")) 63 | -------------------------------------------------------------------------------- /iec_api/iec_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import atexit 3 | import logging 4 | from datetime import datetime 5 | from typing import List, Optional 6 | 7 | import aiofiles 8 | import aiohttp 9 | import jwt 10 | from aiohttp import ClientSession 11 | 12 | from iec_api import commons, data, fault_portal_data, login, masa_data, static_data 13 | from iec_api.fault_portal_models.outages import FaultPortalOutage 14 | from iec_api.fault_portal_models.user_profile import UserProfile 15 | from iec_api.masa_api_models.cities import City 16 | from iec_api.masa_api_models.equipment import GetEquipmentResponse 17 | from iec_api.masa_api_models.lookup import GetLookupResponse 18 | from iec_api.masa_api_models.order_lookup import OrderCategory 19 | from iec_api.masa_api_models.titles import GetTitleResponse 20 | from iec_api.masa_api_models.user_profile import MasaUserProfile 21 | from iec_api.masa_api_models.volt_levels import VoltLevel 22 | from iec_api.models.account import Account 23 | from iec_api.models.contract import Contract 24 | from iec_api.models.contract_check import ContractCheck 25 | from iec_api.models.customer import Customer 26 | from iec_api.models.device import Device, Devices 27 | from iec_api.models.device_identity import DeviceDetails 28 | from iec_api.models.device_type import DeviceType 29 | from iec_api.models.efs import EfsMessage 30 | from iec_api.models.electric_bill import ElectricBill 31 | from iec_api.models.exceptions import IECLoginError 32 | from iec_api.models.invoice import GetInvoicesBody 33 | from iec_api.models.jwt import JWT 34 | from iec_api.models.meter_reading import MeterReadings 35 | from iec_api.models.outages import Outage 36 | from iec_api.models.remote_reading import ReadingResolution, RemoteReadingResponse 37 | from iec_api.usage_calculator.calculator import UsageCalculator 38 | 39 | logger = logging.getLogger(__name__) 40 | 41 | 42 | class IecClient: 43 | """IEC API Client.""" 44 | 45 | def __init__(self, user_id: str | int, session: Optional[ClientSession] = None): 46 | """ 47 | Initializes the class with the provided user ID and optionally logs in automatically. 48 | 49 | Args: 50 | session (ClientSession): The aiohttp ClientSession object. 51 | user_id (str): The user ID (SSN) to be associated with the instance. 52 | automatically_login (bool): Whether to automatically log in the user. Default is False. 53 | """ 54 | 55 | if not commons.is_valid_israeli_id(user_id): 56 | raise ValueError("User ID must be a valid Israeli ID.") 57 | 58 | # Custom Logger to the session 59 | trace_config = aiohttp.TraceConfig() 60 | trace_config.on_request_start.append(commons.on_request_start_debug) 61 | trace_config.on_request_chunk_sent.append(commons.on_request_chunk_sent_debug) 62 | trace_config.on_request_end.append(commons.on_request_end_debug) 63 | trace_config.freeze() 64 | 65 | if not session: 66 | session = aiohttp.ClientSession(trace_configs=[trace_config]) 67 | atexit.register(self._shutdown) 68 | else: 69 | session.trace_configs.append(trace_config) 70 | 71 | self._session = session 72 | 73 | self._state_token: Optional[str] = None # Token for maintaining the state of the user's session 74 | self._factor_id: Optional[str] = None # Factor ID for multifactor authentication 75 | self._session_token: Optional[str] = None # Token for maintaining the user's session 76 | self.logged_in: bool = False # Flag to indicate if the user is logged in 77 | self._token: JWT = JWT( 78 | access_token="", refresh_token="", token_type="", expires_in=0, scope="", id_token="" 79 | ) # Token for authentication 80 | self._user_id: str = str(user_id) # User ID associated with the instance 81 | self._login_response: Optional[str] = None # Response from the login attempt 82 | self._bp_number: Optional[str] = None # BP Number associated with the instance 83 | self._contract_id: Optional[str] = None # Contract ID associated with the instance 84 | self._account_id: Optional[str] = None # Account ID associated with the instance 85 | self._masa_connection_size_map: Optional[dict[int, str]] = None 86 | 87 | def _shutdown(self): 88 | if not self._session.closed: 89 | asyncio.run(self._session.close()) 90 | 91 | # ------------- 92 | # Data methods: 93 | # ------------- 94 | 95 | async def get_customer(self) -> Optional[Customer]: 96 | """ 97 | Get consumer data response from IEC API. 98 | :return: Customer data 99 | """ 100 | await self.check_token() 101 | customer = await data.get_customer(self._session, self._token) 102 | if customer: 103 | self._bp_number = customer.bp_number 104 | return customer 105 | 106 | async def get_accounts(self) -> Optional[List[Account]]: 107 | """ 108 | Get consumer data response from IEC API. 109 | :return: Customer data 110 | """ 111 | await self.check_token() 112 | accounts = await data.get_accounts(self._session, self._token) 113 | 114 | if accounts and len(accounts) > 0: 115 | self._bp_number = accounts[0].account_number 116 | self._account_id = str(accounts[0].id) 117 | 118 | return accounts 119 | 120 | async def get_default_account(self) -> Account: 121 | """ 122 | Get consumer data response from IEC API. 123 | :return: Customer data 124 | """ 125 | accounts = await self.get_accounts() 126 | return accounts[0] 127 | 128 | async def get_default_contract(self, bp_number: str = None) -> Optional[Contract]: 129 | """ 130 | This function retrieves the default contract based on the given BP number. 131 | :param bp_number: A string representing the BP number 132 | :return: Contract object containing the contract information 133 | """ 134 | 135 | await self.check_token() 136 | 137 | if not bp_number: 138 | bp_number = self._bp_number 139 | 140 | assert bp_number, "BP number must be provided" 141 | 142 | get_contract_response = await data.get_contracts(self._session, self._token, bp_number) 143 | if get_contract_response: 144 | contracts = get_contract_response.contracts 145 | if contracts and len(contracts) > 0: 146 | self._contract_id = contracts[0].contract_id 147 | return contracts[0] 148 | return None 149 | 150 | async def get_contracts(self, bp_number: str = None) -> list[Contract]: 151 | """ 152 | This function retrieves a contract based on the given BP number. 153 | :param bp_number: A string representing the BP number 154 | :return: list of Contract objects 155 | """ 156 | 157 | await self.check_token() 158 | 159 | if not bp_number: 160 | bp_number = self._bp_number 161 | 162 | assert bp_number, "BP number must be provided" 163 | 164 | get_contract_response = await data.get_contracts(self._session, self._token, bp_number) 165 | if get_contract_response: 166 | contracts = get_contract_response.contracts 167 | if contracts and len(contracts) > 0: 168 | self._contract_id = contracts[0].contract_id 169 | return contracts 170 | return [] 171 | 172 | async def get_contract_check(self, contract_id: Optional[str] = None) -> Optional[ContractCheck]: 173 | """ 174 | Get contract check for the contract 175 | Args: 176 | self: The instance of the class. 177 | contract_id (str): The Contract ID of the meter. 178 | Returns: 179 | ContractCheck: a contract check 180 | """ 181 | await self.check_token() 182 | 183 | if not contract_id: 184 | contract_id = self._contract_id 185 | 186 | assert contract_id, "Contract Id must be provided" 187 | 188 | return await data.get_contract_check(self._session, self._token, contract_id) 189 | 190 | async def get_last_meter_reading( 191 | self, bp_number: Optional[str] = None, contract_id: Optional[str] = None 192 | ) -> Optional[MeterReadings]: 193 | """ 194 | Retrieves a last meter reading for a specific contract and user. 195 | Args: 196 | self: The instance of the class. 197 | bp_number (str): The BP number of the meter. 198 | contract_id (str): The contract ID associated with the meter. 199 | Returns: 200 | MeterReadings: The response containing the meter readings. 201 | """ 202 | await self.check_token() 203 | if not bp_number: 204 | bp_number = self._bp_number 205 | 206 | assert bp_number, "BP number must be provided" 207 | 208 | if not contract_id: 209 | contract_id = self._contract_id 210 | 211 | assert contract_id, "Contract ID must be provided" 212 | 213 | return await data.get_last_meter_reading(self._session, self._token, bp_number, contract_id) 214 | 215 | async def get_electric_bill( 216 | self, bp_number: Optional[str] = None, contract_id: Optional[str] = None 217 | ) -> Optional[ElectricBill]: 218 | """ 219 | Retrieves a remote reading for a specific meter using the provided parameters. 220 | Args: 221 | self: The instance of the class. 222 | bp_number (str): The BP number of the meter. 223 | contract_id (str): The contract ID associated with the meter. 224 | Returns: 225 | ElectricBill: The Invoices/Electric Bills for the user with the contract_id 226 | """ 227 | await self.check_token() 228 | 229 | if not bp_number: 230 | bp_number = self._bp_number 231 | 232 | assert bp_number, "BP number must be provided" 233 | 234 | if not contract_id: 235 | contract_id = self._contract_id 236 | 237 | assert contract_id, "Contract ID must be provided" 238 | 239 | return await data.get_electric_bill(self._session, self._token, bp_number, contract_id) 240 | 241 | async def save_invoice_pdf_to_file( 242 | self, 243 | file_path: str, 244 | invoice_number: str | int, 245 | bp_number: Optional[str | int] = None, 246 | contract_id: Optional[str | int] = None, 247 | ): 248 | """ 249 | Get PDF of invoice from IEC api 250 | Args: 251 | self: The instance of the class. 252 | file_path (str): Path to save the bill to 253 | invoice_number (str): The requested invoice number 254 | bp_number (str): The BP number of the meter. 255 | contract_id (str): The contract ID associated with the meter. 256 | """ 257 | await self.check_token() 258 | 259 | if not bp_number: 260 | bp_number = self._bp_number 261 | 262 | assert bp_number, "BP number must be provided" 263 | 264 | if not contract_id: 265 | contract_id = self._contract_id 266 | 267 | assert contract_id, "Contract ID must be provided" 268 | 269 | response_bytes = await data.get_invoice_pdf(self._session, self._token, bp_number, contract_id, invoice_number) 270 | if response_bytes: 271 | async with aiofiles.open(file_path, "wb") as f: 272 | await f.write(response_bytes) 273 | 274 | async def send_consumption_report_to_mail( 275 | self, 276 | email: str, 277 | contract_id: Optional[str | int] = None, 278 | device_id: Optional[str | int] = None, 279 | device_code: Optional[str | int] = None, 280 | ) -> bool: 281 | """ 282 | Send Consumption Report to Mail 283 | Args: 284 | self: The instance of the class. 285 | email (str): Email to send the report to 286 | contract_id (str): The contract ID associated with the meter. 287 | device_id: Meter Id, 288 | device_code: Meter Code 289 | """ 290 | await self.check_token() 291 | 292 | if not contract_id: 293 | contract_id = self._contract_id 294 | 295 | assert contract_id, "Contract ID must be provided" 296 | 297 | if not device_id: 298 | devices = await self.get_devices() 299 | 300 | assert devices, "No Devices found" 301 | device_id = devices[0].device_number 302 | 303 | assert device_id, "Device ID must be provided" 304 | 305 | if not device_code: 306 | devices = await self.get_devices() 307 | 308 | assert devices, "No Devices found" 309 | device_code = devices[0].device_code 310 | 311 | assert device_code, "Device Code must be provided" 312 | 313 | return await data.send_consumption_report_to_mail( 314 | self._session, self._token, contract_id, email, device_code, device_id 315 | ) 316 | 317 | async def get_devices(self, contract_id: Optional[str] = None) -> Optional[List[Device]]: 318 | """ 319 | Get a list of devices for the user 320 | Args: 321 | self: The instance of the class. 322 | contract_id (str): The Contract ID of the meter. 323 | Returns: 324 | list[Device]: List of devices 325 | """ 326 | await self.check_token() 327 | 328 | if not contract_id: 329 | contract_id = self._contract_id 330 | 331 | assert contract_id, "Contract Id must be provided" 332 | 333 | return await data.get_devices(self._session, self._token, contract_id) 334 | 335 | async def get_device_by_device_id(self, device_id: str, contract_id: Optional[str] = None) -> Optional[Devices]: 336 | """ 337 | Get a list of devices for the user 338 | Args: 339 | self: The instance of the class. 340 | device_id (str): The Device code. 341 | contract_id (str): The Contract ID of the user. 342 | Returns: 343 | list[Device]: List of devices 344 | """ 345 | await self.check_token() 346 | 347 | if not contract_id: 348 | contract_id = self._contract_id 349 | 350 | assert contract_id, "Contract ID must be provided" 351 | 352 | return await data.get_device_by_device_id(self._session, self._token, contract_id, device_id) 353 | 354 | async def get_device_details_by_device_id(self, device_id: str) -> Optional[List[DeviceDetails]]: 355 | """ 356 | Get a list of devices for the user 357 | Args: 358 | self: The instance of the class. 359 | device_id (str): The Device id. 360 | Returns: 361 | list[DeviceDetails]: List of device details or None 362 | """ 363 | await self.check_token() 364 | 365 | return await data.get_device_details(self._session, self._token, device_id) 366 | 367 | async def get_device_details_by_device_id_and_code( 368 | self, device_id: str, device_code: str 369 | ) -> Optional[DeviceDetails]: 370 | """ 371 | Get a list of devices for the user 372 | Args: 373 | self: The instance of the class. 374 | device_id (str): The Device id. 375 | device_code (str): The Device code. 376 | Returns: 377 | DeviceDetails: Device details or None 378 | """ 379 | await self.check_token() 380 | 381 | return await data.get_device_details_by_code(self._session, self._token, device_id, device_code) 382 | 383 | async def get_remote_reading( 384 | self, 385 | meter_serial_number: str, 386 | meter_code: int, 387 | last_invoice_date: datetime, 388 | from_date: datetime, 389 | resolution: ReadingResolution = ReadingResolution.DAILY, 390 | contract_id: Optional[str] = None, 391 | ) -> Optional[RemoteReadingResponse]: 392 | """ 393 | Retrieves a remote reading for a specific meter using the provided parameters. 394 | Args: 395 | self: The instance of the class. 396 | meter_serial_number (str): The serial number of the meter. 397 | meter_code (int): The code associated with the meter. 398 | last_invoice_date (str): The date of the last invoice. 399 | from_date (str): The start date for the remote reading. 400 | resolution (int): The resolution of the remote reading. 401 | contract_id (str): The contract id. 402 | Returns: 403 | RemoteReadingResponse: The response containing the remote reading or None if not found 404 | """ 405 | await self.check_token() 406 | if not contract_id: 407 | contract_id = self._contract_id 408 | 409 | return await data.get_remote_reading( 410 | self._session, 411 | self._token, 412 | contract_id, 413 | meter_serial_number, 414 | meter_code, 415 | last_invoice_date, 416 | from_date, 417 | resolution, 418 | ) 419 | 420 | async def get_device_type( 421 | self, bp_number: Optional[str] = None, contract_id: Optional[str] = None 422 | ) -> Optional[DeviceType]: 423 | """ 424 | Get a list of devices for the user 425 | Args: 426 | self: The instance of the class. 427 | bp_number (str): The BP number of the meter. 428 | contract_id (str: The Contract ID 429 | Returns: 430 | DeviceType 431 | """ 432 | await self.check_token() 433 | 434 | if not bp_number: 435 | bp_number = self._bp_number 436 | 437 | assert bp_number, "BP number must be provided" 438 | 439 | if not contract_id: 440 | contract_id = self._contract_id 441 | 442 | assert contract_id, "Contract ID must be provided" 443 | 444 | return await data.get_device_type(self._session, self._token, bp_number, contract_id) 445 | 446 | async def get_billing_invoices( 447 | self, bp_number: Optional[str] = None, contract_id: Optional[str] = None 448 | ) -> Optional[GetInvoicesBody]: 449 | """ 450 | Get a list of devices for the user 451 | Args: 452 | self: The instance of the class. 453 | bp_number (str): The BP number of the meter. 454 | contract_id (str: The Contract ID 455 | Returns: 456 | Billing Invoices data 457 | """ 458 | await self.check_token() 459 | 460 | if not bp_number: 461 | bp_number = self._bp_number 462 | 463 | assert bp_number, "BP number must be provided" 464 | 465 | if not contract_id: 466 | contract_id = self._contract_id 467 | 468 | assert contract_id, "Contract ID must be provided" 469 | 470 | return await data.get_billing_invoices(self._session, self._token, bp_number, contract_id) 471 | 472 | async def get_kwh_tariff(self) -> float: 473 | """Get kWh tariff""" 474 | return await static_data.get_kwh_tariff(self._session) 475 | 476 | async def get_distribution_tariff(self, phase_count: Optional[int] = None) -> float: 477 | """Get get_distribution tariff""" 478 | 479 | if not phase_count: 480 | devices = await self.get_devices() 481 | 482 | assert devices, "No Devices found" 483 | device = devices[0] 484 | 485 | device_details = await self.get_device_by_device_id(device.device_number) 486 | assert device_details, "No Device Details" 487 | 488 | phase_count = device_details.counter_devices[0].connection_size.phase 489 | 490 | return await static_data.get_distribution_tariff(self._session, phase_count) 491 | 492 | async def get_delivery_tariff(self, phase_count: Optional[int] = None) -> float: 493 | """Get delivery tariff""" 494 | 495 | if not phase_count: 496 | devices = await self.get_devices() 497 | 498 | assert devices, "No Devices found" 499 | device = devices[0] 500 | 501 | device_details = await self.get_device_by_device_id(device.device_number) 502 | assert device_details, "No Device Details" 503 | 504 | phase_count = device_details.counter_devices[0].connection_size.phase 505 | 506 | return await static_data.get_delivery_tariff(self._session, phase_count) 507 | 508 | async def get_kva_tariff(self) -> float: 509 | """Get KVA tariff""" 510 | return await static_data.get_kva_tariff(self._session) 511 | 512 | async def get_power_size(self, connection: Optional[str] = None) -> float: 513 | """Get delivery tariff""" 514 | 515 | if not connection: 516 | devices = await self.get_devices() 517 | 518 | assert devices, "No Devices found" 519 | device = devices[0] 520 | 521 | device_details = await self.get_device_by_device_id(device.device_number) 522 | assert device_details, "No Device Details" 523 | 524 | connection = device_details.counter_devices[0].connection_size.representative_connection_size 525 | 526 | if "X" not in connection: # Solve cases where the connection size is "25" 527 | connection = "1X" + connection 528 | 529 | return await static_data.get_power_size(self._session, connection) 530 | 531 | async def get_usage_calculator(self) -> UsageCalculator: 532 | """ 533 | Get Usage Calculator module 534 | Returns: 535 | UsageCalculator 536 | """ 537 | return await static_data.get_usage_calculator(self._session) 538 | 539 | async def get_efs_messages( 540 | self, contract_id: Optional[str] = None, service_code: Optional[int] = None 541 | ) -> Optional[List[EfsMessage]]: 542 | """Get EFS Messages for the contract 543 | Args: 544 | self: The instance of the class. 545 | contract_id (str): The Contract ID of the meter. 546 | service_code (str): Specific EFS Service code 547 | Returns: 548 | list[EfsMessage]: List of EFS Messages 549 | """ 550 | await self.check_token() 551 | 552 | if not contract_id: 553 | contract_id = self._contract_id 554 | 555 | assert contract_id, "Contract Id must be provided" 556 | 557 | return await data.get_efs_messages(self._session, self._token, contract_id, service_code) 558 | 559 | async def get_outages_by_account(self, account_id: Optional[str] = None) -> Optional[List[Outage]]: 560 | """Get Outages for the Account 561 | Args: 562 | self: The instance of the class. 563 | account_id (str): The Account ID of the meter. 564 | Returns: 565 | list[Outage]: List of the Outages Messages 566 | """ 567 | await self.check_token() 568 | 569 | if not account_id: 570 | account_id = self._account_id 571 | 572 | assert account_id, "Account Id must be provided" 573 | 574 | return await data.get_outages_by_account(self._session, self._token, account_id) 575 | 576 | # ---------------- 577 | # Masa API Flow 578 | # ---------------- 579 | 580 | async def get_masa_equipment_by_account(self, account_id: Optional[str] = None) -> GetEquipmentResponse: 581 | """Get Equipment for the Account 582 | Args: 583 | self: The instance of the class. 584 | account_id (str): The Account ID of the meter. 585 | Returns: 586 | GetEquipmentResponse: Get Equipment Response 587 | """ 588 | await self.check_token() 589 | 590 | if not account_id: 591 | account_id = self._account_id 592 | 593 | assert account_id, "Account Id must be provided" 594 | 595 | return await masa_data.get_masa_equipments(self._session, self._token, account_id) 596 | 597 | async def get_masa_user_profile(self) -> MasaUserProfile: 598 | """Get Masa User Profile 599 | Args: 600 | self: The instance of the class. 601 | Returns: 602 | MasaUserProfile: Masa User Profile 603 | """ 604 | await self.check_token() 605 | 606 | return await masa_data.get_masa_user_profile(self._session, self._token) 607 | 608 | async def get_masa_cities(self) -> List[City]: 609 | """Get Masa Cities 610 | Args: 611 | self: The instance of the class. 612 | Returns: 613 | list[City]: List of Cities 614 | """ 615 | await self.check_token() 616 | 617 | return await masa_data.get_masa_cities(self._session, self._token) 618 | 619 | async def get_masa_order_categories(self) -> List[OrderCategory]: 620 | """Get Masa Cities for the Account 621 | Args: 622 | self: The instance of the class. 623 | Returns: 624 | list[OrderCategory]: List of Order Categories 625 | """ 626 | await self.check_token() 627 | 628 | return await masa_data.get_masa_order_categories(self._session, self._token) 629 | 630 | async def get_masa_volt_levels(self) -> List[VoltLevel]: 631 | """Get Masa Volt Level 632 | Args: 633 | self: The instance of the class. 634 | Returns: 635 | list[VoltLevel]: List of Volt Levelts 636 | """ 637 | await self.check_token() 638 | return await masa_data.get_masa_volt_levels(self._session, self._token) 639 | 640 | async def get_masa_order_titles(self, account_id: Optional[str] = None) -> GetTitleResponse: 641 | """Get Masa Cities 642 | Args: 643 | self: The instance of the class. 644 | account_id (str): The Account ID of the meter. 645 | Returns: 646 | GetTitleResponse: Get Title Response 647 | """ 648 | await self.check_token() 649 | 650 | if not account_id: 651 | account_id = self._account_id 652 | 653 | assert account_id, "Account Id must be provided" 654 | 655 | return await masa_data.get_masa_order_titles(self._session, self._token, account_id) 656 | 657 | async def get_masa_lookup(self) -> GetLookupResponse: 658 | """Get Masa Lookup 659 | Args: 660 | self: The instance of the class. 661 | Returns: 662 | GetLookupResponse: Get Title Response 663 | """ 664 | await self.check_token() 665 | return await masa_data.get_masa_lookup(self._session, self._token) 666 | 667 | async def get_masa_connection_size_from_masa(self, account_id: Optional[str] = None) -> Optional[str]: 668 | if not self._masa_connection_size_map: 669 | lookup = await self.get_masa_lookup() 670 | self._masa_connection_size_map = {obj.size_type: obj.name for obj in lookup.connection_size_types} 671 | 672 | equipment = await self.get_masa_equipment_by_account(account_id) 673 | 674 | if not equipment or len(equipment.items) < 1 or len(equipment.items[0].connections) < 1: 675 | return None 676 | 677 | connection_size = equipment.items[0].connections[0].power_connection_size 678 | 679 | return self._masa_connection_size_map.get(connection_size) 680 | 681 | # ---------------- 682 | # Fault Portal Endpoints 683 | # ---------------- 684 | 685 | async def get_fault_portal_user_profile(self) -> Optional[UserProfile]: 686 | """Get User Profile for the Account from Fault Portal 687 | Args: 688 | self: The instance of the class. 689 | Returns: 690 | list[UserProfile]: The User Profile 691 | """ 692 | await self.check_token() 693 | 694 | return await fault_portal_data.get_user_profile(self._session, self._token) 695 | 696 | async def get_fault_portal_outages_by_account( 697 | self, account_id: Optional[str] = None 698 | ) -> Optional[List[FaultPortalOutage]]: 699 | """Get Outages for the Account from Fault Portal 700 | Args: 701 | self: The instance of the class. 702 | account_id (str): The Account ID of the meter. 703 | Returns: 704 | list[Outage]: List of the Outages Messages 705 | """ 706 | await self.check_token() 707 | 708 | if not account_id: 709 | account_id = self._account_id 710 | 711 | assert account_id, "Account Id must be provided" 712 | 713 | return await fault_portal_data.get_outages_by_account(self._session, self._token, account_id) 714 | 715 | # ---------------- 716 | # Login/Token Flow 717 | # ---------------- 718 | 719 | async def login_with_id(self): 720 | """ 721 | Login with ID and wait for OTP 722 | """ 723 | state_token, factor_id, session_token = self._login_response = await login.first_login( 724 | self._session, self._user_id 725 | ) 726 | self._state_token = state_token 727 | self._factor_id = factor_id 728 | self._session_token = session_token 729 | 730 | async def verify_otp(self, otp_code: str | int) -> bool: 731 | """ 732 | Verify the OTP code and return the token 733 | :param otp_code: The OTP code to be verified 734 | :return: The token 735 | """ 736 | jwt_token = await login.verify_otp_code(self._session, self._factor_id, self._state_token, str(otp_code)) 737 | self._token = jwt_token 738 | self.logged_in = True 739 | return True 740 | 741 | async def manual_login(self): 742 | """ 743 | Logs the user in by obtaining an authorization token, setting the authorization header, 744 | and updating the login status and token attribute. 745 | """ 746 | token = await login.manual_authorization(self._session, self._user_id) 747 | self.logged_in = True 748 | self._token = token 749 | 750 | def get_token(self) -> JWT: 751 | """ 752 | Return the JWT token. 753 | """ 754 | return self._token 755 | 756 | async def load_jwt_token(self, token: JWT): 757 | """ 758 | Set the token and mark the user as logged in. 759 | :param token: The new token to be set. 760 | :return: None 761 | """ 762 | self._token = token 763 | if await self.check_token(): 764 | self.logged_in = True 765 | else: 766 | raise IECLoginError(-1, "Invalid JWT token") 767 | 768 | async def override_id_token(self, id_token): 769 | """ 770 | Set the token and mark the user as logged in. 771 | :param id_token: The new token to be set. 772 | :return: None 773 | """ 774 | logger.debug(f"Overriding jwt.py token: {id_token}") 775 | self._token = JWT(access_token="", refresh_token="", token_type="", expires_in=0, scope="", id_token=id_token) 776 | self._token.id_token = id_token 777 | self.logged_in = True 778 | 779 | async def check_token(self) -> bool: 780 | """ 781 | Check the validity of the jwt.py token and refresh in the case of expired signature errors. 782 | """ 783 | should_refresh = False 784 | 785 | try: 786 | remaining_to_expiration = login.get_token_remaining_time_to_expiration(self._token) 787 | if remaining_to_expiration < 0: 788 | should_refresh = True 789 | 790 | except jwt.exceptions.ExpiredSignatureError as e: 791 | raise IECLoginError(-1, "Expired JWT token") from e 792 | 793 | if should_refresh: 794 | logger.debug("jwt.py token expired, refreshing token") 795 | self.logged_in = False 796 | await self.refresh_token() 797 | 798 | return True 799 | 800 | async def refresh_token(self): 801 | """ 802 | Refresh IEC JWT token. 803 | """ 804 | self._token = await login.refresh_token(self._session, self._token) 805 | if self._token: 806 | self.logged_in = True 807 | 808 | async def load_token_from_file(self, file_path: str = "token.json"): 809 | """ 810 | Load token from file. 811 | """ 812 | self._token = await login.load_token_from_file(file_path) 813 | self.logged_in = True 814 | 815 | async def save_token_to_file(self, file_path: str = "token.json"): 816 | """ 817 | Save token to file. 818 | """ 819 | await login.save_token_to_file(self._token, file_path) 820 | -------------------------------------------------------------------------------- /iec_api/login.py: -------------------------------------------------------------------------------- 1 | """IEC Login Module.""" 2 | 3 | import json 4 | import logging 5 | import random 6 | import re 7 | import string 8 | import time 9 | from typing import Optional, Tuple 10 | 11 | import aiofiles 12 | import jwt 13 | import pkce 14 | from aiohttp import ClientSession 15 | 16 | from iec_api import commons 17 | from iec_api.models.exceptions import IECLoginError 18 | from iec_api.models.jwt import JWT 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | APP_CLIENT_ID = "0oaqf6zr7yEcQZqqt2p7" 23 | CODE_CHALLENGE_METHOD = "S256" 24 | APP_REDIRECT_URI = "com.iecrn:/" 25 | code_verifier, code_challenge = pkce.generate_pkce_pair() 26 | STATE = "".join(random.choice(string.digits + string.ascii_letters) for _ in range(12)) 27 | IEC_OKTA_BASE_URL = "https://iec-ext.okta.com" 28 | 29 | AUTHORIZE_URL = ( 30 | "https://iec-ext.okta.com/oauth2/default/v1/authorize?client_id={" 31 | "client_id}&response_type=id_token+code&response_mode=form_post&scope=openid%20email%20profile" 32 | "%20offline_access&redirect_uri=com.iecrn:/&state=123abc&nonce=abc123&code_challenge_method=S256" 33 | "&sessionToken={sessionToken}&code_challenge={challenge}" 34 | ) 35 | 36 | 37 | async def authorize_session(session: ClientSession, session_token) -> str: 38 | """ 39 | Authorizes a session using the provided session token. 40 | Args: 41 | session: The aiohttp ClientSession object. 42 | session_token (str): The session token to be used for authorization. 43 | Returns: 44 | str: The code obtained from the authorization response. 45 | """ 46 | cmd_url = AUTHORIZE_URL.format(client_id=APP_CLIENT_ID, sessionToken=session_token, challenge=code_challenge) 47 | authorize_response = await commons.send_non_json_get_request( 48 | session=session, url=cmd_url, encoding="unicode-escape" 49 | ) 50 | code = re.findall( 51 | r"", 52 | authorize_response.encode("latin1").decode("utf-8"), 53 | )[0] 54 | return code 55 | 56 | 57 | async def get_first_factor_id(session: ClientSession, user_id: str): 58 | """ 59 | Function to get the first factor ID using the provided ID. 60 | Args: 61 | session: The aiohttp ClientSession object. 62 | user_id (str): The user ID be used for authorization. 63 | Returns: 64 | Tuple[str,str]: [The state token, the factor id] 65 | """ 66 | data = {"username": f"{user_id}@iec.co.il"} 67 | headers = {"accept": "application/json", "content-type": "application/json"} 68 | 69 | response = await commons.send_post_request( 70 | session=session, url=f"{IEC_OKTA_BASE_URL}/api/v1/authn", json_data=data, headers=headers 71 | ) 72 | return response.get("stateToken", ""), response.get("_embedded", {}).get("factors", {})[0].get("id") 73 | 74 | 75 | async def send_otp_code( 76 | session: ClientSession, factor_id: object, state_token: object, pass_code: object = None 77 | ) -> Optional[str]: 78 | """ 79 | Send OTP code for factor verification and return the session token if successful. 80 | 81 | Args: 82 | session: The aiohttp ClientSession object. 83 | factor_id (object): The identifier of the factor for verification. 84 | state_token (object): The state token for the verification process. 85 | pass_code (object, optional): The pass code for verification. Defaults to None. 86 | 87 | Returns: 88 | Optional[str]: The session token if verification is successful, otherwise None. 89 | """ 90 | data = {"stateToken": state_token} 91 | if pass_code: 92 | data["passCode"] = pass_code 93 | headers = {"accept": "application/json", "content-type": "application/json"} 94 | 95 | response = await commons.send_post_request( 96 | session=session, 97 | url=f"{IEC_OKTA_BASE_URL}/api/v1/authn/factors/{factor_id}/verify", 98 | json_data=data, 99 | headers=headers, 100 | ) 101 | if response.get("status") == "SUCCESS": 102 | return response.get("sessionToken") 103 | return None 104 | 105 | 106 | async def get_access_token(session: ClientSession, code) -> JWT: 107 | """ 108 | Get the access token using the provided authorization code. 109 | Args: 110 | session: The aiohttp ClientSession object. 111 | code (str): The authorization code. 112 | Returns: 113 | JWT: The access token as a JWT object. 114 | """ 115 | data = { 116 | "client_id": APP_CLIENT_ID, 117 | "code_verifier": code_verifier, 118 | "grant_type": "authorization_code", 119 | "redirect_uri": APP_REDIRECT_URI, 120 | "code": code, 121 | } 122 | response = await commons.send_post_request( 123 | session=session, url=f"{IEC_OKTA_BASE_URL}/oauth2/default/v1/token", data=data 124 | ) 125 | return JWT.from_dict(response) 126 | 127 | 128 | async def first_login(session: ClientSession, id_number: str) -> Tuple[str, str, str]: 129 | """ 130 | Perform the first login for a user. 131 | 132 | Args: 133 | session: The aiohttp ClientSession object. 134 | id_number (str): The user's ID number. 135 | 136 | Returns: 137 | Tuple[str, str, str]: A tuple containing the state token, factor ID, and session token. 138 | """ 139 | 140 | try: 141 | # Get the first factor ID and state token 142 | state_token, factor_id = await get_first_factor_id(session, id_number) 143 | 144 | # Send OTP code and get session token 145 | session_token = await send_otp_code(session, factor_id, state_token) 146 | 147 | return state_token, factor_id, session_token 148 | except Exception as error: 149 | logger.warning(f"Failed at first login: {error}") 150 | raise IECLoginError(-1, "Failed at first login") from error 151 | 152 | 153 | async def verify_otp_code(session: ClientSession, factor_id: str, state_token: str, otp_code: str) -> JWT: 154 | """ 155 | Verify the OTP code for the given factor_id, state_token, and otp_code and return the JWT. 156 | 157 | Args: 158 | session: The aiohttp ClientSession object. 159 | factor_id (str): The factor ID for the OTP verification. 160 | state_token (str): The state token for the OTP verification. 161 | otp_code (str): The OTP code to be verified. 162 | 163 | Returns: 164 | JWT: The JSON Web Token (JWT) for the authorized session. 165 | """ 166 | try: 167 | otp_session_token = await send_otp_code(session, factor_id, state_token, otp_code) 168 | code = await authorize_session(session, otp_session_token) 169 | jwt_token = await get_access_token(session, code) 170 | return jwt_token 171 | except Exception as error: 172 | logger.warning(f"Failed at OTP verification: {error}") 173 | raise IECLoginError(-1, "Failed at OTP verification") from error 174 | 175 | 176 | async def manual_authorization(session: ClientSession, id_number) -> Optional[JWT]: # pragma: no cover 177 | """Get authorization token from IEC API.""" 178 | if not id_number: 179 | id_number = await commons.read_user_input("Enter your ID Number: ") 180 | state_token, factor_id, session_token = await first_login(session, id_number) 181 | if not state_token: 182 | logger.error("Failed to send OTP") 183 | raise IECLoginError(-1, "Failed to send OTP, no state_token") 184 | 185 | otp_code = await commons.read_user_input("Enter your OTP code: ") 186 | code = await authorize_session(session, otp_code) 187 | jwt_token = await verify_otp_code(session, factor_id, state_token, code) 188 | logger.debug( 189 | f"Access token: {jwt_token.access_token}\n" 190 | f"Refresh token: {jwt_token.refresh_token}\n" 191 | f"id_token: {jwt_token.id_token}" 192 | ) 193 | return jwt_token 194 | 195 | 196 | async def refresh_token(session: ClientSession, token: JWT) -> Optional[JWT]: 197 | """Refresh IEC JWT token.""" 198 | headers = {"accept": "application/json", "content-type": "application/x-www-form-urlencoded"} 199 | data = { 200 | "client_id": APP_CLIENT_ID, 201 | "redirect_uri": APP_REDIRECT_URI, 202 | "refresh_token": token.refresh_token, 203 | "grant_type": "refresh_token", 204 | "scope": "openid email profile offline_access", 205 | } 206 | response = await commons.send_post_request( 207 | session=session, url=f"{IEC_OKTA_BASE_URL}/oauth2/default/v1/token", data=data, headers=headers 208 | ) 209 | return JWT.from_dict(response) 210 | 211 | 212 | async def save_token_to_file(token: JWT, path: str = "token.json") -> None: 213 | """Save token to file.""" 214 | async with aiofiles.open(path, mode="w", encoding="utf-8") as f: 215 | await f.write(json.dumps(token.to_dict())) 216 | 217 | 218 | def decode_token(token: JWT) -> dict: 219 | return jwt.decode(token.id_token, options={"verify_signature": False}, algorithms=["RS256"]) 220 | 221 | 222 | async def load_token_from_file(path: str = "token.json") -> JWT: 223 | """Load token from file.""" 224 | async with aiofiles.open(path, "r", encoding="utf-8") as f: 225 | contents = await f.read() 226 | 227 | jwt_data = JWT.from_dict(json.loads(contents)) 228 | 229 | # decode token to verify validity 230 | decode_token(jwt_data) 231 | 232 | return jwt_data 233 | 234 | 235 | def get_token_remaining_time_to_expiration(token: JWT): 236 | decoded_token = decode_token(token) 237 | return decoded_token["exp"] - int(time.time()) 238 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/building_options.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masaapi-wa.azurewebsites.net/lookup/newConnection/buildingOptions 8 | # 9 | # { 10 | # "residenceOptions": [ 11 | # { 12 | # "orderPurpose": { 13 | # "desc": "בית בודד", 14 | # "buildingType": 1, 15 | # "id": "a31b52d8-68da-e911-a973-000d3a29f080", 16 | # "name": "מגורים צמודי קרקע" 17 | # }, 18 | # "connectionSizes": [ 19 | # { 20 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 21 | # "name": "חד פאזי 40 אמפר (1X40)" 22 | # } //.. 23 | # ] 24 | # }, 25 | # { 26 | # "orderPurpose": { 27 | # "desc": "דו משפחתי", 28 | # "buildingType": 1, 29 | # "id": "ae1b52d8-68da-e911-a973-000d3a29f080", 30 | # "name": "צ.קרקע אחד מדו-משפחתי" 31 | # }, 32 | # "connectionSizes": [ 33 | # { 34 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 35 | # "name": "חד פאזי 40 אמפר (1X40)" 36 | # } //.. 37 | # ] 38 | # } 39 | # ], 40 | # "commercialOptions": [ 41 | # { 42 | # "orderPurpose": { 43 | # "desc": "חיבור אחד בפילר/ מערכת מנייה", 44 | # "buildingType": 3, 45 | # "id": "ab1b52d8-68da-e911-a973-000d3a29f080", 46 | # "name": "לא מגורים בפילר מונים" 47 | # }, 48 | # "connectionSizes": [ 49 | # { 50 | # "id": "7ddab873-671f-e911-a961-000d3a29f1fd", 51 | # "name": "חד פאזי 6 אמפר (1X6)" 52 | # } //.. 53 | # ] 54 | # }, 55 | # { 56 | # "orderPurpose": { 57 | # "desc": "חיבור אחד בפילר לשני מונים", 58 | # "buildingType": 1, 59 | # "id": "b91b52d8-68da-e911-a973-000d3a29f080", 60 | # "name": "לא מגורים בפילר-אחד מתוך דו-משפחתי" 61 | # }, 62 | # "connectionSizes": [ 63 | # { 64 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 65 | # "name": "חד פאזי 40 אמפר (1X40)" 66 | # } //... 67 | # ] 68 | # } 69 | # ], 70 | # "tempOptions": [ 71 | # { 72 | # "orderPurpose": { 73 | # "desc": "חיבור זמני לאתר בניה", 74 | # "buildingType": 6, 75 | # "id": "8f1b52d8-68da-e911-a973-000d3a29f080", 76 | # "name": "חיבור ארעי" 77 | # }, 78 | # "connectionSizes": [ 79 | # { 80 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 81 | # "name": "חד פאזי 40 אמפר (1X40)" 82 | # } //... 83 | # ] 84 | # } 85 | # ], 86 | # "residenceSizeTypes": [ 87 | # { 88 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 89 | # "name": "חד פאזי 40 אמפר (1X40)" 90 | # } // ... 91 | # ], 92 | # "commercialSizeTypes": [ 93 | # { 94 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 95 | # "name": "חד פאזי 40 אמפר (1X40)" 96 | # }, 97 | # { 98 | # "id": "85dab873-671f-e911-a961-000d3a29f1fd", 99 | # "name": "תלת פאזי 25 אמפר (3X25)" 100 | # } // .. 101 | # ], 102 | # "publicSizeTypes": [ 103 | # { 104 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 105 | # "name": "חד פאזי 40 אמפר (1X40)" 106 | # } //... 107 | # ], 108 | # "parkingSizeTypes": [ 109 | # { 110 | # "id": "84dab873-671f-e911-a961-000d3a29f1fd", 111 | # "name": "חד פאזי 40 אמפר (1X40)" 112 | # } //... 113 | # ] 114 | # } 115 | 116 | 117 | @dataclass 118 | class OrderPurpose(DataClassDictMixin): 119 | """ 120 | Represents the purpose of an order related to building types. 121 | 122 | Attributes: 123 | desc (str): A description of the order purpose. 124 | building_type (int): The type of building associated with the order. 125 | id (UUID): The unique identifier for the order purpose. 126 | name (str): The name of the order purpose. 127 | """ 128 | 129 | desc: str = field(metadata=field_options(alias="desc")) 130 | building_type: int = field(metadata=field_options(alias="buildingType")) 131 | id: UUID = field(metadata=field_options(alias="id")) 132 | name: str = field(metadata=field_options(alias="name")) 133 | 134 | 135 | @dataclass 136 | class ConnectionSize(DataClassDictMixin): 137 | """ 138 | Represents a connection size option. 139 | 140 | Attributes: 141 | id (UUID): The unique identifier for the connection size. 142 | name (str): The name of the connection size, typically including phase and amperage details. 143 | """ 144 | 145 | id: UUID = field(metadata=field_options(alias="id")) 146 | name: str = field(metadata=field_options(alias="name")) 147 | 148 | 149 | @dataclass 150 | class ResidenceOption(DataClassDictMixin): 151 | """ 152 | Represents a residential option, including order purpose and available connection sizes. 153 | 154 | Attributes: 155 | order_purpose (OrderPurpose): The purpose of the order for a residential option. 156 | connection_sizes (List[ConnectionSize]): A list of connection sizes available for this residential option. 157 | """ 158 | 159 | order_purpose: OrderPurpose = field(metadata=field_options(alias="orderPurpose")) 160 | connection_sizes: List[ConnectionSize] = field(metadata=field_options(alias="connectionSizes")) 161 | 162 | 163 | @dataclass 164 | class CommercialOption(DataClassDictMixin): 165 | """ 166 | Represents a commercial option, including order purpose and available connection sizes. 167 | 168 | Attributes: 169 | order_purpose (OrderPurpose): The purpose of the order for a commercial option. 170 | connection_sizes (List[ConnectionSize]): A list of connection sizes available for this commercial option. 171 | """ 172 | 173 | order_purpose: OrderPurpose = field(metadata=field_options(alias="orderPurpose")) 174 | connection_sizes: List[ConnectionSize] = field(metadata=field_options(alias="connectionSizes")) 175 | 176 | 177 | @dataclass 178 | class TempOption(DataClassDictMixin): 179 | """ 180 | Represents a temporary option, including order purpose and available connection sizes. 181 | 182 | Attributes: 183 | order_purpose (OrderPurpose): The purpose of the order for a temporary option. 184 | connection_sizes (List[ConnectionSize]): A list of connection sizes available for this temporary option. 185 | """ 186 | 187 | order_purpose: OrderPurpose = field(metadata=field_options(alias="orderPurpose")) 188 | connection_sizes: List[ConnectionSize] = field(metadata=field_options(alias="connectionSizes")) 189 | 190 | 191 | @dataclass 192 | class OptionSizeType(DataClassDictMixin): 193 | """ 194 | Represents a size type option for various building categories. 195 | 196 | Attributes: 197 | id (UUID): The unique identifier for the size type. 198 | name (str): The name of the size type, typically including phase and amperage details. 199 | """ 200 | 201 | id: UUID = field(metadata=field_options(alias="id")) 202 | name: str = field(metadata=field_options(alias="name")) 203 | 204 | 205 | @dataclass 206 | class GetBuildingOptionsResponse(DataClassDictMixin): 207 | """ 208 | Represents the overall response structure containing various options for residential, commercial, 209 | and temporary connections. 210 | 211 | Attributes: 212 | residence_options (List[ResidenceOption]): A list of residential options available. 213 | commercial_options (List[CommercialOption]): A list of commercial options available. 214 | temp_options (List[TempOption]): A list of temporary options available. 215 | residence_size_types (List[OptionSizeType]): A list of size types available for residential buildings. 216 | commercial_size_types (List[OptionSizeType]): A list of size types available for commercial buildings. 217 | public_size_types (List[OptionSizeType]): A list of size types available for public buildings. 218 | parking_size_types (List[OptionSizeType]): A list of size types available for parking facilities. 219 | """ 220 | 221 | residence_options: List[ResidenceOption] = field(metadata=field_options(alias="residenceOptions")) 222 | commercial_options: List[CommercialOption] = field(metadata=field_options(alias="commercialOptions")) 223 | temp_options: List[TempOption] = field(metadata=field_options(alias="tempOptions")) 224 | residence_size_types: List[OptionSizeType] = field(metadata=field_options(alias="residenceSizeTypes")) 225 | commercial_size_types: List[OptionSizeType] = field(metadata=field_options(alias="commercialSizeTypes")) 226 | public_size_types: List[OptionSizeType] = field(metadata=field_options(alias="publicSizeTypes")) 227 | parking_size_types: List[OptionSizeType] = field(metadata=field_options(alias="parkingSizeTypes")) 228 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/cities.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masa-mainportalapi.iec.co.il/api/cities 8 | # 9 | # { 10 | # "dataCollection": [ 11 | # { 12 | # "area": { 13 | # "name": "עמקים", 14 | # "shovalAreaCode": 702, 15 | # "id": "15414b54-d8db-ea11-a813-000d3aabca53", 16 | # "logicalName": "iec_area" 17 | # }, 18 | # "region": { 19 | # "name": "חיפה והצפון", 20 | # "shovalRegionCode": 7, 21 | # "id": "909a5d57-d7db-ea11-a813-000d3aabca53", 22 | # "logicalName": "iec_region" 23 | # }, 24 | # "name": "אלון תבור", 25 | # "shovalCityCode": "2054", 26 | # "id": "34698a40-28ec-ea11-a817-000d3a239ca0", 27 | # "logicalName": "iec_city" 28 | # } // ... 29 | # ] 30 | # } 31 | 32 | 33 | @dataclass 34 | class Area(DataClassDictMixin): 35 | """ 36 | Represents an area within a region. 37 | 38 | Attributes: 39 | name (str): The name of the area. 40 | shoval_area_code (int): The Shoval area code. 41 | id (UUID): The unique identifier for the area. 42 | logical_name (str): The logical name of the area entity. 43 | """ 44 | 45 | name: str = field(metadata=field_options(alias="name")) 46 | shoval_area_code: int = field(metadata=field_options(alias="shovalAreaCode")) 47 | id: UUID = field(metadata=field_options(alias="id")) 48 | logical_name: str = field(metadata=field_options(alias="logicalName")) 49 | 50 | 51 | @dataclass 52 | class Region(DataClassDictMixin): 53 | """ 54 | Represents a region. 55 | 56 | Attributes: 57 | name (str): The name of the region. 58 | shoval_region_code (int): The Shoval region code. 59 | id (UUID): The unique identifier for the region. 60 | logical_name (str): The logical name of the region entity. 61 | """ 62 | 63 | name: str = field(metadata=field_options(alias="name")) 64 | shoval_region_code: int = field(metadata=field_options(alias="shovalRegionCode")) 65 | id: UUID = field(metadata=field_options(alias="id")) 66 | logical_name: str = field(metadata=field_options(alias="logicalName")) 67 | 68 | 69 | @dataclass 70 | class City(DataClassDictMixin): 71 | """ 72 | Represents a city within a region and area. 73 | 74 | Attributes: 75 | area (Area): The area to which the city belongs. 76 | region (Region): The region to which the city belongs. 77 | name (str): The name of the city. 78 | shoval_city_code (str): The Shoval city code. 79 | id (UUID): The unique identifier for the city. 80 | logical_name (str): The logical name of the city entity. 81 | """ 82 | 83 | area: Area = field(metadata=field_options(alias="area")) 84 | region: Region = field(metadata=field_options(alias="region")) 85 | name: str = field(metadata=field_options(alias="name")) 86 | shoval_city_code: str = field(metadata=field_options(alias="shovalCityCode")) 87 | id: UUID = field(metadata=field_options(alias="id")) 88 | logical_name: str = field(metadata=field_options(alias="logicalName")) 89 | 90 | 91 | @dataclass 92 | class CitiesResponse(DataClassDictMixin): 93 | """ 94 | Represents the response containing a list of cities. 95 | 96 | Attributes: 97 | data_collection (List[City]): A list of cities. 98 | """ 99 | 100 | data_collection: List[City] = field(metadata=field_options(alias="dataCollection")) 101 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/equipment.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List, Optional 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masaapi-wa.azurewebsites.net/equipments/get?accountId={account_id}&pageNumber=1&pageSize=10 8 | 9 | # { 10 | # { 11 | # "pageSize": 10, 12 | # "pageNumber": 1, 13 | # "moreRecords": false, 14 | # "totalRecords": 1, 15 | # "pageCookie": 0, 16 | # "items": [ 17 | # { 18 | # "accountId": "b67ea524-300f-4a85-9ef4-54d30a753452", 19 | # "accountType": 279830001, 20 | # "accountName": "זינגר מיטל", 21 | # "addressId": "b67ea524-300f-4a85-9ef4-54d30a753452", 22 | # "areaId": "b67ea524-300f-4a85-9ef4-54d30a753452", 23 | # "regionId": "b67ea524-300f-4a85-9ef4-54d30a753452", 24 | # "fullAddress": "פרישמן 47 ,תל אביב - יפו, קומה ב", 25 | # "siteId": "b67ea524-300f-4a85-9ef4-54d30a753452", 26 | # "iec_ContractNumber": "346669815", 27 | # "siteType": 18, 28 | # "activeConnections": 1, 29 | # "connections": [ 30 | # { 31 | # "connectionId": "b67ea524-300f-4a85-9ef4-54d30a753452", 32 | # "connectionNumber": "12345", 33 | # "hasOpenOrders": false, 34 | # "shovalOpenOrder": null, 35 | # "powerConnectionSize": 279830006, 36 | # "isLowVolte": true, 37 | # "isCanIncrease": false, 38 | # "meterId": "b67ea524-300f-4a85-9ef4-54d30a753452", 39 | # "meterNumber": "1234", 40 | # "voltType": 279830001, 41 | # "currentReader": "17.32", 42 | # "currentAmpere": null, 43 | # "currentVoltLevel": { 44 | # "id": null, 45 | # "name": null 46 | # } 47 | # } 48 | # ], 49 | # "possible2ShiftBit": false, 50 | # "building": { 51 | # "id": "b67ea524-300f-4a85-9ef4-54d30a753452", 52 | # "name": "700045035", 53 | # "hasSAOrders": null, 54 | # "shovalSAOrder": null, 55 | # "multiResidentialBuilding": true, 56 | # "isMaxPublicSitesSum": false, 57 | # "isResidentialBuilding": false, 58 | # "numOfRelatedSitesInt": 9 59 | # }, 60 | # "backuplineOrder": null, 61 | # "hasBackuplineOrders": false 62 | # } 63 | # ] 64 | # } 65 | # } 66 | 67 | 68 | @dataclass 69 | class VoltLevel(DataClassDictMixin): 70 | """ 71 | Represents the voltage level details of a connection. 72 | 73 | Attributes: 74 | id (Optional[str]): The ID of the voltage level. 75 | name (Optional[str]): The name of the voltage level. 76 | """ 77 | 78 | id: Optional[str] = field(metadata=field_options(alias="id")) 79 | name: Optional[str] = field(metadata=field_options(alias="name")) 80 | 81 | 82 | @dataclass 83 | class Connection(DataClassDictMixin): 84 | """ 85 | Represents a connection within an account item. 86 | 87 | Attributes: 88 | connection_id (UUID): The ID of the connection. 89 | connection_number (str): The number of the connection. 90 | has_open_orders (bool): Whether the connection has open orders. 91 | shoval_open_order (Optional[str]): The open order associated with the connection. 92 | power_connection_size (int): The size of the power connection. 93 | is_low_volte (bool): Indicates if the connection is low voltage. 94 | is_can_increase (bool): Indicates if the power connection size can be increased. 95 | meter_id (UUID): The ID of the meter associated with the connection. 96 | meter_number (str): The number of the meter. 97 | volt_type (int): The type of voltage. 98 | current_reader (str): The current reading of the connection. 99 | current_ampere (Optional[str]): The current ampere value. 100 | current_volt_level (VoltLevel): The voltage level details of the connection. 101 | """ 102 | 103 | connection_id: UUID = field(metadata=field_options(alias="connectionId")) 104 | connection_number: str = field(metadata=field_options(alias="connectionNumber")) 105 | has_open_orders: bool = field(metadata=field_options(alias="hasOpenOrders")) 106 | shoval_open_order: Optional[str] = field(metadata=field_options(alias="shovalOpenOrder")) 107 | power_connection_size: int = field(metadata=field_options(alias="powerConnectionSize")) 108 | is_low_volte: bool = field(metadata=field_options(alias="isLowVolte")) 109 | is_can_increase: bool = field(metadata=field_options(alias="isCanIncrease")) 110 | meter_id: UUID = field(metadata=field_options(alias="meterId")) 111 | meter_number: str = field(metadata=field_options(alias="meterNumber")) 112 | volt_type: int = field(metadata=field_options(alias="voltType")) 113 | current_reader: str = field(metadata=field_options(alias="currentReader")) 114 | current_ampere: Optional[str] = field(metadata=field_options(alias="currentAmpere")) 115 | current_volt_level: VoltLevel = field(metadata=field_options(alias="currentVoltLevel")) 116 | 117 | 118 | @dataclass 119 | class Building(DataClassDictMixin): 120 | """ 121 | Represents the building details associated with an account item. 122 | 123 | Attributes: 124 | id (UUID): The ID of the building. 125 | name (str): The name of the building. 126 | has_sa_orders (Optional[str]): Indicates if there are SA orders for the building. 127 | shoval_sa_order (Optional[str]): The SA order associated with the building. 128 | multi_residential_building (bool): Indicates if the building is a multi-residential building. 129 | is_max_public_sites_sum (bool): Indicates if the building has reached the maximum public sites sum. 130 | is_residential_building (bool): Indicates if the building is residential. 131 | num_of_related_sites_int (int): The number of related sites within the building. 132 | """ 133 | 134 | id: UUID = field(metadata=field_options(alias="id")) 135 | name: str = field(metadata=field_options(alias="name")) 136 | has_sa_orders: Optional[str] = field(metadata=field_options(alias="hasSAOrders")) 137 | shoval_sa_order: Optional[str] = field(metadata=field_options(alias="shovalSAOrder")) 138 | multi_residential_building: bool = field(metadata=field_options(alias="multiResidentialBuilding")) 139 | is_max_public_sites_sum: bool = field(metadata=field_options(alias="isMaxPublicSitesSum")) 140 | is_residential_building: bool = field(metadata=field_options(alias="isResidentialBuilding")) 141 | num_of_related_sites_int: int = field(metadata=field_options(alias="numOfRelatedSitesInt")) 142 | 143 | 144 | @dataclass 145 | class Item(DataClassDictMixin): 146 | """ 147 | Represents an individual account item within the response. 148 | 149 | Attributes: 150 | account_id (UUID): The ID of the account. 151 | account_type (int): The type of the account. 152 | account_name (str): The name of the account holder. 153 | address_id (UUID): The ID of the address associated with the account. 154 | area_id (UUID): The ID of the area associated with the account. 155 | region_id (UUID): The ID of the region associated with the account. 156 | full_address (str): The full address of the account holder. 157 | site_id (UUID): The ID of the site associated with the account. 158 | iec_contract_number (str): The IEC contract number. 159 | site_type (int): The type of the site. 160 | active_connections (int): The number of active connections associated with the account. 161 | connections (List[Connection]): A list of connections associated with the account. 162 | possible_2_shift_bit (bool): Indicates if the account is eligible for possible shift bit. 163 | building (Building): The building details associated with the account. 164 | backupline_order (Optional[str]): The backupline order associated with the account. 165 | has_backupline_orders (bool): Indicates if the account has backupline orders. 166 | """ 167 | 168 | account_id: UUID = field(metadata=field_options(alias="accountId")) 169 | account_type: int = field(metadata=field_options(alias="accountType")) 170 | account_name: str = field(metadata=field_options(alias="accountName")) 171 | address_id: UUID = field(metadata=field_options(alias="addressId")) 172 | area_id: UUID = field(metadata=field_options(alias="areaId")) 173 | region_id: UUID = field(metadata=field_options(alias="regionId")) 174 | full_address: str = field(metadata=field_options(alias="fullAddress")) 175 | site_id: UUID = field(metadata=field_options(alias="siteId")) 176 | iec_contract_number: str = field(metadata=field_options(alias="iec_ContractNumber")) 177 | site_type: int = field(metadata=field_options(alias="siteType")) 178 | active_connections: int = field(metadata=field_options(alias="activeConnections")) 179 | connections: List[Connection] = field(metadata=field_options(alias="connections")) 180 | possible_2_shift_bit: bool = field(metadata=field_options(alias="possible2ShiftBit")) 181 | building: Building = field(metadata=field_options(alias="building")) 182 | backupline_order: Optional[str] = field(metadata=field_options(alias="backuplineOrder")) 183 | has_backupline_orders: bool = field(metadata=field_options(alias="hasBackuplineOrders")) 184 | 185 | 186 | @dataclass 187 | class GetEquipmentResponse(DataClassDictMixin): 188 | """ 189 | Represents the overall response structure. 190 | 191 | Attributes: 192 | page_size (int): The number of items per page. 193 | page_number (int): The current page number. 194 | more_records (bool): Indicates if there are more records available. 195 | total_records (int): The total number of records available. 196 | page_cookie (int): A cookie value for pagination. 197 | items (List[Item]): A list of account items included in the response. 198 | """ 199 | 200 | page_size: int = field(metadata=field_options(alias="pageSize")) 201 | page_number: int = field(metadata=field_options(alias="pageNumber")) 202 | more_records: bool = field(metadata=field_options(alias="moreRecords")) 203 | total_records: int = field(metadata=field_options(alias="totalRecords")) 204 | page_cookie: int = field(metadata=field_options(alias="pageCookie")) 205 | items: List[Item] = field(metadata=field_options(alias="items")) 206 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/lookup.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List, Optional 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masaapi-wa.azurewebsites.net/lookup/all 8 | # 9 | # { 10 | # "regions": [ 11 | # { 12 | # "regionId": "909a5d57-d7db-ea11-a813-000d3aabca53", 13 | # "name": "חיפה והצפון", 14 | # "code": 7 15 | # } //... 16 | # ], 17 | # "connectionSizeTypes": [ 18 | # { 19 | # "id": "7ddab873-671f-e911-a961-000d3a29f1fd", 20 | # "name": "6", 21 | # "sizeType": 279830001, 22 | # "voltType": 279830001, 23 | # "code": "Z01", 24 | # "description": "חד פאזי 6 אמפר (1X6)", 25 | # "index": 1, 26 | # "isEnlargeable": true, 27 | # "isAllowResidence": false 28 | # } //... 29 | # ], 30 | # "siteTypes": [ 31 | # { 32 | # "key": 20, 33 | # "value": "_20", 34 | # "index": 0 35 | # } //... 36 | # ], 37 | # "orderStatusState": [ 38 | # { 39 | # "key": 279830000, 40 | # "value": "לא החל", 41 | # "index": 1 42 | # } //..., 43 | # ], 44 | # "actionCodes": [ 45 | # { 46 | # "key": 1, 47 | # "value": "חיבורים חדשים", 48 | # "index": 1 49 | # } // ... 50 | # ], 51 | # "phonePrefixes": [ 52 | # { 53 | # "key": 50, 54 | # "value": "050", 55 | # "index": 0 56 | # } // ... 57 | # ], 58 | # "orderPurposes": [ 59 | # { 60 | # "desc": "מסחרי אחר", 61 | # "buildingType": 4, 62 | # "id": "811b52d8-68da-e911-a973-000d3a29f080", 63 | # "name": "מסחרי ואחר" 64 | # } // ... 65 | # ], 66 | # "meterSetupTypes": [ 67 | # { 68 | # "id": "5c3544e4-68da-e911-a973-000d3a29f080", 69 | # "name": "ריכוז בכניסה לבניין ", 70 | # "desc": "ריכוז בכניסה לבניין", 71 | # "code": "Z1" 72 | # } // ... 73 | # ], 74 | # "stateMachineForOrderStage": null 75 | # } 76 | 77 | 78 | @dataclass 79 | class Region(DataClassDictMixin): 80 | """ 81 | Represents a geographic region. 82 | 83 | Attributes: 84 | region_id (UUID): The unique identifier for the region. 85 | name (str): The name of the region. 86 | code (int): The code associated with the region. 87 | """ 88 | 89 | region_id: UUID = field(metadata=field_options(alias="regionId")) 90 | name: str = field(metadata=field_options(alias="name")) 91 | code: int = field(metadata=field_options(alias="code")) 92 | 93 | 94 | @dataclass 95 | class ConnectionSizeType(DataClassDictMixin): 96 | """ 97 | Represents the type and specifications of a connection size. 98 | 99 | Attributes: 100 | id (UUID): The unique identifier for the connection size type. 101 | name (str): The name of the connection size type. 102 | size_type (int): The size type code. 103 | volt_type (int): The voltage type code. 104 | code (str): The code associated with the connection size type. 105 | description (str): A description of the connection size type. 106 | index (int): The index order of the connection size type. 107 | is_enlargeable (bool): Indicates if the connection size type can be enlarged. 108 | is_allow_residence (Optional[bool]): Indicates if the connection size type is allowed for residential use. 109 | """ 110 | 111 | id: UUID = field(metadata=field_options(alias="id")) 112 | name: str = field(metadata=field_options(alias="name")) 113 | size_type: int = field(metadata=field_options(alias="sizeType")) 114 | volt_type: int = field(metadata=field_options(alias="voltType")) 115 | code: str = field(metadata=field_options(alias="code")) 116 | description: str = field(metadata=field_options(alias="description")) 117 | index: int = field(metadata=field_options(alias="index")) 118 | is_enlargeable: bool = field(metadata=field_options(alias="isEnlargeable")) 119 | is_allow_residence: Optional[bool] = field(metadata=field_options(alias="isAllowResidence")) 120 | 121 | 122 | @dataclass 123 | class SiteType(DataClassDictMixin): 124 | """ 125 | Represents a type of site. 126 | 127 | Attributes: 128 | key (int): The unique key for the site type. 129 | value (str): The value or name of the site type. 130 | index (int): The index order of the site type. 131 | """ 132 | 133 | key: int = field(metadata=field_options(alias="key")) 134 | value: str = field(metadata=field_options(alias="value")) 135 | index: int = field(metadata=field_options(alias="index")) 136 | 137 | 138 | @dataclass 139 | class OrderStatusState(DataClassDictMixin): 140 | """ 141 | Represents the status of an order. 142 | 143 | Attributes: 144 | key (int): The unique key for the order status. 145 | value (str): The value or name of the order status. 146 | index (int): The index order of the order status. 147 | """ 148 | 149 | key: int = field(metadata=field_options(alias="key")) 150 | value: str = field(metadata=field_options(alias="value")) 151 | index: int = field(metadata=field_options(alias="index")) 152 | 153 | 154 | @dataclass 155 | class ActionCode(DataClassDictMixin): 156 | """ 157 | Represents an action code for operations. 158 | 159 | Attributes: 160 | key (int): The unique key for the action code. 161 | value (str): The value or name of the action code. 162 | index (int): The index order of the action code. 163 | """ 164 | 165 | key: int = field(metadata=field_options(alias="key")) 166 | value: str = field(metadata=field_options(alias="value")) 167 | index: int = field(metadata=field_options(alias="index")) 168 | 169 | 170 | @dataclass 171 | class PhonePrefix(DataClassDictMixin): 172 | """ 173 | Represents a phone prefix. 174 | 175 | Attributes: 176 | key (int): The unique key for the phone prefix. 177 | value (str): The value or number of the phone prefix. 178 | index (int): The index order of the phone prefix. 179 | """ 180 | 181 | key: int = field(metadata=field_options(alias="key")) 182 | value: str = field(metadata=field_options(alias="value")) 183 | index: int = field(metadata=field_options(alias="index")) 184 | 185 | 186 | @dataclass 187 | class OrderPurpose(DataClassDictMixin): 188 | """ 189 | Represents the purpose of an order. 190 | 191 | Attributes: 192 | desc (str): The description of the order purpose. 193 | building_type (Optional[int]): The type of building associated with the order purpose. 194 | id (UUID): The unique identifier for the order purpose. 195 | name (str): The name of the order purpose. 196 | """ 197 | 198 | desc: str = field(metadata=field_options(alias="desc")) 199 | building_type: Optional[int] = field(metadata=field_options(alias="buildingType")) 200 | id: UUID = field(metadata=field_options(alias="id")) 201 | name: str = field(metadata=field_options(alias="name")) 202 | 203 | 204 | @dataclass 205 | class MeterSetupType(DataClassDictMixin): 206 | """ 207 | Represents the setup type of a meter. 208 | 209 | Attributes: 210 | id (UUID): The unique identifier for the meter setup type. 211 | name (str): The name of the meter setup type. 212 | desc (str): A description of the meter setup type. 213 | code (str): The code associated with the meter setup type. 214 | """ 215 | 216 | id: UUID = field(metadata=field_options(alias="id")) 217 | name: str = field(metadata=field_options(alias="name")) 218 | desc: str = field(metadata=field_options(alias="desc")) 219 | code: str = field(metadata=field_options(alias="code")) 220 | 221 | 222 | @dataclass 223 | class GetLookupResponse(DataClassDictMixin): 224 | """ 225 | Represents the overall response structure containing various types of data. 226 | 227 | Attributes: 228 | regions (List[Region]): A list of geographic regions. 229 | connection_size_types (List[ConnectionSizeType]): A list of connection size types. 230 | site_types (List[SiteType]): A list of site types. 231 | order_status_state (List[OrderStatusState]): A list of order status states. 232 | action_codes (List[ActionCode]): A list of action codes. 233 | phone_prefixes (List[PhonePrefix]): A list of phone prefixes. 234 | order_purposes (List[OrderPurpose]): A list of order purposes. 235 | meter_setup_types (List[MeterSetupType]): A list of meter setup types. 236 | state_machine_for_order_stage (Optional[str]): A placeholder for the state machine for order stages. 237 | """ 238 | 239 | regions: List[Region] = field(metadata=field_options(alias="regions")) 240 | connection_size_types: List[ConnectionSizeType] = field(metadata=field_options(alias="connectionSizeTypes")) 241 | site_types: List[SiteType] = field(metadata=field_options(alias="siteTypes")) 242 | order_status_state: List[OrderStatusState] = field(metadata=field_options(alias="orderStatusState")) 243 | action_codes: List[ActionCode] = field(metadata=field_options(alias="actionCodes")) 244 | phone_prefixes: List[PhonePrefix] = field(metadata=field_options(alias="phonePrefixes")) 245 | order_purposes: List[OrderPurpose] = field(metadata=field_options(alias="orderPurposes")) 246 | meter_setup_types: List[MeterSetupType] = field(metadata=field_options(alias="meterSetupTypes")) 247 | state_machine_for_order_stage: Optional[str] = field(metadata=field_options(alias="stateMachineForOrderStage")) 248 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/order_lookup.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masaapi-wa.azurewebsites.net/api/orderLookup 8 | # 9 | # { 10 | # "orderCategories": [ 11 | # { 12 | # "name": "הגדלת חיבור ממתח נמוך למתח גבוה", 13 | # "typeCodeInt": 1, 14 | # "id": "234b41b5-61c4-ee11-907a-000d3a2e4333" 15 | # } //... 16 | # ] 17 | # } 18 | 19 | 20 | @dataclass 21 | class OrderCategory(DataClassDictMixin): 22 | """ 23 | Represents a category of an order. 24 | 25 | Attributes: 26 | name (str): The name of the order category. 27 | type_code_int (int): The type code associated with the order category. 28 | id (UUID): The unique identifier for the order category. 29 | """ 30 | 31 | name: str = field(metadata=field_options(alias="name")) 32 | type_code_int: int = field(metadata=field_options(alias="typeCodeInt")) 33 | id: UUID = field(metadata=field_options(alias="id")) 34 | 35 | 36 | @dataclass 37 | class OrderLookupResponse(DataClassDictMixin): 38 | """ 39 | Represents the response containing a list of order categories. 40 | 41 | Attributes: 42 | order_categories (List[OrderCategory]): A list of order categories. 43 | """ 44 | 45 | order_categories: List[OrderCategory] = field(metadata=field_options(alias="orderCategories")) 46 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/titles.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masaapi-wa.azurewebsites.net/api/accounts/{account_id}/orders/titles 8 | 9 | # 10 | # {"orders":[],"id":"ebee08af-9972-e811-8106-3863bb358f68"} 11 | 12 | 13 | @dataclass 14 | class Order(DataClassDictMixin): 15 | """ 16 | Represents an order. 17 | 18 | Attributes: 19 | ? 20 | """ 21 | 22 | 23 | @dataclass 24 | class GetTitleResponse(DataClassDictMixin): 25 | """ 26 | Represents a title response 27 | 28 | Attributes: 29 | id (UUID): id 30 | orders (List[Order]) : list of orders 31 | """ 32 | 33 | id: UUID = field(metadata=field_options(alias="id")) 34 | orders: List[Order] = field(metadata=field_options(alias="orders")) 35 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/user_profile.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masa-mainportalapi.iec.co.il/api/contacts/userprofile 8 | # 9 | # { 10 | # "governmentid": "1234566", 11 | # "idType": 279830001, 12 | # "email": "my.mail@gmail.com", 13 | # "phoneNumber": "123456", 14 | # "accounts": [ 15 | # { 16 | # "name": "השם שלי", 17 | # "accountNumber": "12345", 18 | # "governmentNumber": "12345", 19 | # "accountType": 279830000, 20 | # "viewTypeCode": 1, 21 | # "identificationType": 279830001, 22 | # "consumptionOrderViewTypeCode": 1, 23 | # "id": "b67ea524-300f-4a85-9ef4-54d30a753452", 24 | # "logicalName": "account" 25 | # } 26 | # ], 27 | # "phonePrefix": "051", 28 | # "isConnectedToPrivateAccount": false, 29 | # "isAccountOwner": false, 30 | # "isAccountContact": false, 31 | # "id": "b67ea524-300f-4a85-9ef4-54d30a753452", 32 | # "logicalName": "contact" 33 | # } 34 | 35 | 36 | @dataclass 37 | class UserProfileAccount(DataClassDictMixin): 38 | """ 39 | Represents an account associated with a contact. 40 | 41 | Attributes: 42 | name (str): The name of the account. 43 | account_number (str): The account number. 44 | government_number (str): The government ID associated with the account. 45 | account_type (int): The type of the account. 46 | view_type_code (int): The view type code for the account. 47 | identification_type (int): The type of identification used for the account. 48 | consumption_order_view_type_code (int): The view type code for consumption order. 49 | id (UUID): The unique identifier for the account. 50 | logical_name (str): The logical name of the entity. 51 | """ 52 | 53 | name: str = field(metadata=field_options(alias="name")) 54 | account_number: str = field(metadata=field_options(alias="accountNumber")) 55 | government_number: str = field(metadata=field_options(alias="governmentNumber")) 56 | account_type: int = field(metadata=field_options(alias="accountType")) 57 | view_type_code: int = field(metadata=field_options(alias="viewTypeCode")) 58 | identification_type: int = field(metadata=field_options(alias="identificationType")) 59 | consumption_order_view_type_code: int = field(metadata=field_options(alias="consumptionOrderViewTypeCode")) 60 | id: UUID = field(metadata=field_options(alias="id")) 61 | logical_name: str = field(metadata=field_options(alias="logicalName")) 62 | 63 | 64 | @dataclass 65 | class MasaUserProfile(DataClassDictMixin): 66 | """ 67 | Represents a contact with associated accounts. 68 | 69 | Attributes: 70 | government_id (str): The government ID of the contact. 71 | id_type (int): The type of ID used. 72 | email (str): The email address of the contact. 73 | phone_number (str): The phone number of the contact. 74 | accounts (List[Account]): A list of accounts associated with the contact. 75 | phone_prefix (str): The phone prefix of the contact. 76 | is_connected_to_private_account (bool): Indicates if the contact is connected to a private account. 77 | is_account_owner (bool): Indicates if the contact is the owner of the account. 78 | is_account_contact (bool): Indicates if the contact is an account contact. 79 | id (UUID): The unique identifier for the contact. 80 | logical_name (str): The logical name of the entity. 81 | """ 82 | 83 | government_id: str = field(metadata=field_options(alias="governmentid")) 84 | id_type: int = field(metadata=field_options(alias="idType")) 85 | email: str = field(metadata=field_options(alias="email")) 86 | phone_number: str = field(metadata=field_options(alias="phoneNumber")) 87 | accounts: List[UserProfileAccount] = field(metadata=field_options(alias="accounts")) 88 | phone_prefix: str = field(metadata=field_options(alias="phonePrefix")) 89 | is_connected_to_private_account: bool = field(metadata=field_options(alias="isConnectedToPrivateAccount")) 90 | is_account_owner: bool = field(metadata=field_options(alias="isAccountOwner")) 91 | is_account_contact: bool = field(metadata=field_options(alias="isAccountContact")) 92 | id: UUID = field(metadata=field_options(alias="id")) 93 | logical_name: str = field(metadata=field_options(alias="logicalName")) 94 | -------------------------------------------------------------------------------- /iec_api/masa_api_models/volt_levels.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://masaapi-wa.azurewebsites.net/api/voltLevels/active 8 | # 9 | # { 10 | # "dataCollection": [ 11 | # { 12 | # "name": "12.6", 13 | # "minVoltLevel": 29, 14 | # "maxVoltLevel": 400, 15 | # "id": "1b7d1650-4eb1-ed11-9885-000d3a2e4333" 16 | # }, 17 | # { 18 | # "name": "22", 19 | # "minVoltLevel": 17, 20 | # "maxVoltLevel": 400, 21 | # "id": "1d7d1650-4eb1-ed11-9885-000d3a2e4333" 22 | # }, 23 | # { 24 | # "name": "33", 25 | # "minVoltLevel": 11, 26 | # "maxVoltLevel": 400, 27 | # "id": "1f7d1650-4eb1-ed11-9885-000d3a2e4333" 28 | # } 29 | # ] 30 | # } 31 | 32 | 33 | @dataclass 34 | class VoltLevel(DataClassDictMixin): 35 | """ 36 | Represents a voltage level. 37 | 38 | Attributes: 39 | name (str): The name or label of the voltage level. 40 | min_volt_level (int): The minimum voltage level. 41 | max_volt_level (int): The maximum voltage level. 42 | id (UUID): The unique identifier for the voltage level. 43 | """ 44 | 45 | name: str = field(metadata=field_options(alias="name")) 46 | min_volt_level: int = field(metadata=field_options(alias="minVoltLevel")) 47 | max_volt_level: int = field(metadata=field_options(alias="maxVoltLevel")) 48 | id: UUID = field(metadata=field_options(alias="id")) 49 | 50 | 51 | @dataclass 52 | class VoltLevelsResponse(DataClassDictMixin): 53 | """ 54 | Represents the response containing a list of voltage levels. 55 | 56 | Attributes: 57 | data_collection (List[VoltLevel]): A list of voltage levels. 58 | """ 59 | 60 | data_collection: List[VoltLevel] = field(metadata=field_options(alias="dataCollection")) 61 | -------------------------------------------------------------------------------- /iec_api/masa_data.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from aiohttp import ClientSession 4 | 5 | from iec_api import commons 6 | from iec_api.const import ( 7 | GET_MASA_CITIES_LOOKUP_URL, 8 | GET_MASA_EQUIPMENTS_URL, 9 | GET_MASA_LOOKUP_URL, 10 | GET_MASA_ORDER_LOOKUP_URL, 11 | GET_MASA_ORDER_TITLES_URL, 12 | GET_MASA_USER_PROFILE_LOOKUP_URL, 13 | GET_MASA_VOLT_LEVELS_URL, 14 | HEADERS_WITH_AUTH, 15 | ) 16 | from iec_api.masa_api_models.cities import CitiesResponse, City 17 | from iec_api.masa_api_models.equipment import GetEquipmentResponse 18 | from iec_api.masa_api_models.lookup import GetLookupResponse 19 | from iec_api.masa_api_models.order_lookup import OrderCategory, OrderLookupResponse 20 | from iec_api.masa_api_models.titles import GetTitleResponse 21 | from iec_api.masa_api_models.user_profile import MasaUserProfile 22 | from iec_api.masa_api_models.volt_levels import VoltLevel, VoltLevelsResponse 23 | from iec_api.models.jwt import JWT 24 | 25 | cities = None 26 | order_categories = None 27 | volt_levels = None 28 | lookup = None 29 | 30 | 31 | async def get_masa_cities(session: ClientSession, token: JWT) -> List[City]: 32 | """Get Cities from IEC Masa API.""" 33 | 34 | global cities 35 | if not cities: 36 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 37 | # sending get request and saving the response as response object 38 | response = await commons.send_get_request(session=session, url=GET_MASA_CITIES_LOOKUP_URL, headers=headers) 39 | 40 | cities = CitiesResponse.from_dict(response).data_collection 41 | return cities 42 | 43 | 44 | async def get_masa_order_categories(session: ClientSession, token: JWT) -> List[OrderCategory]: 45 | """Get Order Categories from IEC Masa API.""" 46 | 47 | global order_categories 48 | if not order_categories: 49 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 50 | # sending get request and saving the response as response object 51 | response = await commons.send_get_request(session=session, url=GET_MASA_ORDER_LOOKUP_URL, headers=headers) 52 | 53 | order_categories = OrderLookupResponse.from_dict(response).order_categories 54 | return order_categories 55 | 56 | 57 | async def get_masa_user_profile(session: ClientSession, token: JWT) -> MasaUserProfile: 58 | """Get User Profile from IEC Masa API.""" 59 | 60 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 61 | # sending get request and saving the response as response object 62 | response = await commons.send_get_request(session=session, url=GET_MASA_USER_PROFILE_LOOKUP_URL, headers=headers) 63 | 64 | return MasaUserProfile.from_dict(response) 65 | 66 | 67 | async def get_masa_equipments(session: ClientSession, token: JWT, account_id: str) -> GetEquipmentResponse: 68 | """Get Equipments from IEC Masa API.""" 69 | 70 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 71 | # sending get request and saving the response as response object 72 | response = await commons.send_get_request( 73 | session=session, url=GET_MASA_EQUIPMENTS_URL.format(account_id=account_id), headers=headers 74 | ) 75 | 76 | return GetEquipmentResponse.from_dict(response) 77 | 78 | 79 | async def get_masa_volt_levels(session: ClientSession, token: JWT) -> List[VoltLevel]: 80 | """Get Volt Levels from IEC Masa API.""" 81 | 82 | global volt_levels 83 | if not volt_levels: 84 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 85 | # sending get request and saving the response as response object 86 | response = await commons.send_get_request(session=session, url=GET_MASA_VOLT_LEVELS_URL, headers=headers) 87 | 88 | volt_levels = VoltLevelsResponse.from_dict(response).data_collection 89 | return volt_levels 90 | 91 | 92 | async def get_masa_order_titles(session: ClientSession, token: JWT, account_id: str) -> GetTitleResponse: 93 | """Get Order Title from IEC Masa API.""" 94 | 95 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 96 | # sending get request and saving the response as response object 97 | response = await commons.send_get_request( 98 | session=session, url=GET_MASA_ORDER_TITLES_URL.format(account_id=account_id), headers=headers 99 | ) 100 | 101 | return GetTitleResponse.from_dict(response) 102 | 103 | 104 | async def get_masa_lookup(session: ClientSession, token: JWT) -> GetLookupResponse: 105 | """Get All Lookup from IEC Masa API.""" 106 | global lookup 107 | if not lookup: 108 | headers = commons.add_auth_bearer_to_headers(HEADERS_WITH_AUTH, token.id_token) 109 | # sending get request and saving the response as response object 110 | response = await commons.send_get_request(session=session, url=GET_MASA_LOOKUP_URL, headers=headers) 111 | 112 | lookup = GetLookupResponse.from_dict(response) 113 | return lookup 114 | -------------------------------------------------------------------------------- /iec_api/models/account.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional 3 | from uuid import UUID 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | from mashumaro.codecs import BasicDecoder 7 | 8 | from iec_api.models.response_descriptor import ResponseWithDescriptor 9 | 10 | # GET https://iecapi.iec.co.il//api/outages/accounts 11 | # 12 | # 13 | # { 14 | # "data": [ 15 | # { 16 | # "accountNumber": "123", 17 | # "accountType": 1234, 18 | # "id": "ebee08af-1111-2222-3333-3863bb358f68", 19 | # "email": null, 20 | # "telephone": null, 21 | # "governmentNumber": "200461929", 22 | # "name": "My Name", 23 | # "viewTypeCode": 1 24 | # } 25 | # ], 26 | # "reponseDescriptor": { 27 | # "isSuccess": true, 28 | # "code": "0", 29 | # "description": "" 30 | # } 31 | # } 32 | 33 | 34 | @dataclass 35 | class Account(DataClassDictMixin): 36 | account_number: str = field(metadata=field_options(alias="accountNumber")) 37 | account_type: int = field(metadata=field_options(alias="accountType")) 38 | id: UUID 39 | government_number: str = field(metadata=field_options(alias="governmentNumber")) 40 | name: str 41 | view_type_code: int = field(metadata=field_options(alias="viewTypeCode")) 42 | email: Optional[str] = field(default=None, metadata=field_options(alias="email")) 43 | telephone: Optional[str] = field(default=None, metadata=field_options(alias="telephone")) 44 | 45 | 46 | decoder = BasicDecoder(ResponseWithDescriptor[list[Account]]) 47 | -------------------------------------------------------------------------------- /iec_api/models/contract.py: -------------------------------------------------------------------------------- 1 | """Contract model.""" 2 | 3 | from dataclasses import dataclass, field 4 | from datetime import date 5 | 6 | from mashumaro import DataClassDictMixin, field_options 7 | from mashumaro.codecs import BasicDecoder 8 | 9 | from iec_api.models.response_descriptor import ResponseWithDescriptor 10 | 11 | # GET https://iecapi.iec.co.il//api/customer/contract/{bp_number}?count=1 12 | # 13 | # 14 | # { 15 | # "data": { 16 | # "contracts": [ 17 | # { 18 | # "address": "הכתובת שלי", 19 | # "contractId": "0001234", 20 | # "dueDate": "1900-01-01", 21 | # "totalDebt": 0.0, 22 | # "frequency": 2, 23 | # "status": 1, 24 | # "fromPativteProducer": false, 25 | # "cityCode": "000000001864", 26 | # "cityName": "תל אביב - יפו", 27 | # "streetCode": "00000000111", 28 | # "streetName": "נמיר", 29 | # "houseNumber": "22", 30 | # "debtForInvoicesDueDateNotPassed": 0.0, 31 | # "isTouz": false, 32 | # "smartMeter": false, 33 | # "producerType": 1, 34 | # "isDomestic": true, 35 | # } 36 | # ], 37 | # "contractAmount": 1, 38 | # "totalToPay": 0.0 39 | # }, 40 | # "reponseDescriptor": { 41 | # "isSuccess": true, 42 | # "code": "0", 43 | # "description": "" 44 | # } 45 | # } 46 | 47 | 48 | @dataclass 49 | class Contract(DataClassDictMixin): 50 | address: str 51 | contract_id: str = field(metadata=field_options(alias="contractId")) 52 | due_date: date = field(metadata=field_options(alias="dueDate")) 53 | total_debt: float = field(metadata=field_options(alias="totalDebt")) 54 | frequency: int 55 | status: int 56 | from_private_producer: bool = field(metadata=field_options(alias="fromPativteProducer")) 57 | city_code: str = field(metadata=field_options(alias="cityCode")) 58 | city_name: str = field(metadata=field_options(alias="cityName")) 59 | street_code: str = field(metadata=field_options(alias="streetCode")) 60 | street_name: str = field(metadata=field_options(alias="streetName")) 61 | house_number: str = field(metadata=field_options(alias="houseNumber")) 62 | debt_for_invoices_due_date_not_passed: float = field( 63 | metadata=field_options(alias="debtForInvoicesDueDateNotPassed") 64 | ) 65 | is_touz: bool = field(metadata=field_options(alias="isTouz")) 66 | smart_meter: bool = field(metadata=field_options(alias="smartMeter")) 67 | producer_type: int = field(metadata=field_options(alias="producerType")) 68 | is_domestic: bool = field(metadata=field_options(alias="isDomestic")) 69 | 70 | 71 | @dataclass 72 | class Contracts(DataClassDictMixin): 73 | contracts: list[Contract] 74 | contract_amount: int = field(metadata=field_options(alias="contractAmount")) 75 | total_to_pay: float = field(metadata=field_options(alias="totalToPay")) 76 | 77 | 78 | decoder = BasicDecoder(ResponseWithDescriptor[Contracts]) 79 | -------------------------------------------------------------------------------- /iec_api/models/contract_check.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from enum import IntEnum 3 | from typing import Optional 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | from mashumaro.codecs import BasicDecoder 7 | 8 | from iec_api.models.response_descriptor import ResponseWithDescriptor 9 | 10 | # Type = 4/6 11 | # 12 | # GET https://iecapi.iec.co.il//api/customer/checkContract/{{contract_id}}/{type} 13 | # 14 | # Response: 15 | # 16 | # { 17 | # "data": { 18 | # "contractNumber": "123456", 19 | # "deviceNumber": "1234566", 20 | # "bpKind": "0001", 21 | # "bp": "12345", 22 | # "address": "הכתובת שלי", 23 | # "city": "תל אביב - יפו", 24 | # "cityCode": "000000001234", 25 | # "streetCode": "000000001234", 26 | # "houseNumber": "01", 27 | # "bpType": "01", 28 | # "isPrivate": "", 29 | # "hasDirectDebit": false, 30 | # "isMatam": false, 31 | # "frequency": 2 // 1 = Monthly, 2 = BiMonthly 32 | # }, 33 | # "reponseDescriptor": { 34 | # "isSuccess": true, 35 | # "code": "00", 36 | # "description": "" 37 | # } 38 | # } 39 | # 40 | 41 | 42 | class InvoiceFrequency(IntEnum): 43 | MONTHLY = 1 44 | BIMONTHLY = 2 45 | 46 | 47 | @dataclass 48 | class ContractCheck(DataClassDictMixin): 49 | contract_number: str = field(metadata=field_options(alias="contractNumber")) 50 | device_number: str = field(metadata=field_options(alias="deviceNumber")) 51 | bp_kind: str = field(metadata=field_options(alias="bpKind")) 52 | bp: str = field(metadata=field_options(alias="bp")) 53 | address: str = field(metadata=field_options(alias="address")) 54 | city: str = field(metadata=field_options(alias="city")) 55 | city_code: str = field(metadata=field_options(alias="cityCode")) # based on CityCode request 56 | street_code: str = field(metadata=field_options(alias="streetCode")) # based on StreetCode request 57 | house_number: str = field(metadata=field_options(alias="houseNumber")) 58 | bp_type: str = field(metadata=field_options(alias="bpType")) 59 | is_private: str = field(metadata=field_options(alias="isPrivate")) # "X" if private producer 60 | has_direct_debit: bool = field(metadata=field_options(alias="hasDirectDebit")) 61 | is_matam: bool = field(metadata=field_options(alias="isMatam")) 62 | frequency: Optional[InvoiceFrequency] = field(default=None, metadata=field_options(alias="frequency")) 63 | 64 | 65 | decoder = BasicDecoder(ResponseWithDescriptor[ContractCheck]) 66 | -------------------------------------------------------------------------------- /iec_api/models/customer.py: -------------------------------------------------------------------------------- 1 | """Customer model.""" 2 | 3 | # URL: 4 | # POST https://iecapi.iec.co.il/api/customer 5 | # Headers: Authorization: Bearer ... 6 | 7 | # Response: 8 | # { 9 | # "bpNumber": "...", 10 | # "idType": 1, 11 | # "accounts": [ 12 | # { 13 | # "mainContractId": "...", 14 | # "mainContractIdType": 1, 15 | # "companyId": "...", 16 | # "name": "...", 17 | # "lastName": "...", 18 | # "bpNumber": "...", 19 | # "bpType": 1, 20 | # "isActiveAccount": true, 21 | # "customerRole": 0, 22 | # "accountType": 1 23 | # } 24 | # ], 25 | # "customerStatus": 1, 26 | # "idNumber": "...", 27 | # "firstName": "...", 28 | # "lastName": "...", 29 | # "mobilePhone": "...", 30 | # "email": "..." 31 | # } 32 | 33 | from dataclasses import dataclass, field 34 | from typing import Optional 35 | 36 | from mashumaro import DataClassDictMixin, field_options 37 | 38 | 39 | @dataclass 40 | class CustomerAccount(DataClassDictMixin): 41 | main_contract_id: str = field(metadata=field_options(alias="mainContractId")) 42 | main_contract_id_type: int = field(metadata=field_options(alias="mainContractIdType")) 43 | company_id: str = field(metadata=field_options(alias="companyId")) 44 | bp_number: str = field(metadata=field_options(alias="bpNumber")) 45 | bp_type: int = field(metadata=field_options(alias="bpType")) 46 | is_active_account: bool = field(metadata=field_options(alias="isActiveAccount")) 47 | customer_role: int = field(metadata=field_options(alias="customerRole")) 48 | account_type: int = field(metadata=field_options(alias="accountType")) 49 | name: Optional[str] = field(default=None, metadata=field_options(alias="name")) 50 | last_name: Optional[str] = field(default=None, metadata=field_options(alias="lastName")) 51 | 52 | 53 | @dataclass 54 | class Customer(DataClassDictMixin): 55 | bp_number: str = field(metadata=field_options(alias="bpNumber")) 56 | id_type: int = field(metadata=field_options(alias="idType")) 57 | accounts: list[CustomerAccount] 58 | customer_status: int = field(metadata=field_options(alias="customerStatus")) 59 | id_number: str = field(metadata=field_options(alias="idNumber")) 60 | first_name: str = field(metadata=field_options(alias="firstName")) 61 | last_name: str = field(metadata=field_options(alias="lastName")) 62 | mobile_phone: str = field(metadata=field_options(alias="mobilePhone")) 63 | email: str 64 | -------------------------------------------------------------------------------- /iec_api/models/device.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import date 3 | from typing import Optional 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | from mashumaro.codecs import BasicDecoder 7 | 8 | from iec_api.models.response_descriptor import ResponseWithDescriptor 9 | 10 | # 11 | # GET https://iecapi.iec.co.il//api/Device/{contract_id} 12 | # 13 | # [ 14 | # { 15 | # "isActive": true, 16 | # "deviceType": 3, 17 | # "deviceNumber": "23231111", 18 | # "deviceCode": "503" 19 | # } 20 | # 21 | # ----------- 22 | # GET https://iecapi.iec.co.il//api/Device/{bp_number}/{contract_id} 23 | # { 24 | # "data": { 25 | # "counterDevices": [ 26 | # { 27 | # "device": "00000000002000111", 28 | # "register": "001", 29 | # "lastMR": "00000000000011111", 30 | # "lastMRDate": "2024-01-11", 31 | # "lastMRType": "01", 32 | # "lastMRTypeDesc": "קריאת מונה לפי שגרות מערכת", 33 | # "connectionSize": { 34 | # "size": 25, 35 | # "phase": 3, 36 | # "representativeConnectionSize": "3X20" 37 | # } 38 | # } 39 | # ], 40 | # "mrType": "01" 41 | # }, 42 | # "reponseDescriptor": { 43 | # "isSuccess": true, 44 | # "code": "00", 45 | # "description": null 46 | # } 47 | # } 48 | 49 | 50 | @dataclass 51 | class Device(DataClassDictMixin): 52 | """Device dataclass.""" 53 | 54 | is_active: bool = field(metadata=field_options(alias="isActive")) 55 | device_type: Optional[int] = field(default=None, metadata=field_options(alias="deviceType")) 56 | device_number: Optional[str] = field(default=None, metadata=field_options(alias="deviceNumber")) 57 | device_code: Optional[str] = field(default=None, metadata=field_options(alias="deviceCode")) 58 | 59 | 60 | @dataclass 61 | class ConnectionSize(DataClassDictMixin): 62 | """Connection dataclass.""" 63 | 64 | size: int = field(metadata=field_options(alias="size")) 65 | phase: int = field(metadata=field_options(alias="phase")) 66 | representative_connection_size: str = field(metadata=field_options(alias="representativeConnectionSize")) 67 | 68 | 69 | @dataclass 70 | class CounterDevice(DataClassDictMixin): 71 | """Counter Device dataclass.""" 72 | 73 | device: str = field(metadata=field_options(alias="device")) 74 | register: str = field(metadata=field_options(alias="register")) 75 | last_mr: str = field(metadata=field_options(alias="lastMR")) 76 | last_mr_type: str = field(metadata=field_options(alias="lastMRType")) 77 | last_mr_type_desc: str = field(metadata=field_options(alias="lastMRTypeDesc")) 78 | connection_size: ConnectionSize = field(metadata=field_options(alias="connectionSize")) 79 | last_mr_date: Optional[date] = field(default=None, metadata=field_options(alias="lastMRDate")) 80 | 81 | 82 | @dataclass 83 | class Devices(DataClassDictMixin): 84 | """Devices dataclass.""" 85 | 86 | mr_type: str = field(metadata=field_options(alias="mrType")) 87 | counter_devices: Optional[list[CounterDevice]] = field(default=None, metadata=field_options(alias="counterDevices")) 88 | 89 | 90 | decoder = BasicDecoder(ResponseWithDescriptor[Devices]) 91 | -------------------------------------------------------------------------------- /iec_api/models/device_identity.py: -------------------------------------------------------------------------------- 1 | # GET https://iecapi.iec.co.il//api/Tenant/Identify/{{device_id}} 2 | from dataclasses import dataclass, field 3 | from datetime import datetime 4 | from typing import Optional 5 | 6 | from mashumaro import DataClassDictMixin, field_options 7 | from mashumaro.codecs import BasicDecoder 8 | 9 | from iec_api.models.response_descriptor import ResponseWithDescriptor 10 | 11 | # { 12 | # "data": { 13 | # "devicesDetails": [ 14 | # { 15 | # "deviceNumber": "12345", 16 | # "deviceCode": "123", 17 | # "address": "הרצל 5, חיפה", 18 | # "lastReadingValue": "12345", 19 | # "lastReadingType": "01", 20 | # "lastReadingDate": "2024-03-01T00:00:00" 21 | # } 22 | # ], 23 | # "lastDate": "0001-01-01T00:00:00", 24 | # "privateProducer": false 25 | # }, 26 | # "reponseDescriptor": { 27 | # "isSuccess": true, 28 | # "code": "26", 29 | # "description": "מונה אינו חד חד ערכי" 30 | # } 31 | # } 32 | 33 | 34 | @dataclass 35 | class DeviceDetails(DataClassDictMixin): 36 | """Device Details dataclass""" 37 | 38 | device_number: str = field(metadata=field_options(alias="deviceNumber")) 39 | device_code: str = field(metadata=field_options(alias="deviceCode")) 40 | address: str = field(metadata=field_options(alias="address")) 41 | last_reading_value: str = field(metadata=field_options(alias="lastReadingValue")) 42 | last_reading_type: str = field(metadata=field_options(alias="lastReadingType")) 43 | last_reading_date: datetime = field(metadata=field_options(alias="lastReadingDate")) 44 | 45 | 46 | @dataclass 47 | class DeviceIdentity(DataClassDictMixin): 48 | """Devices dataclass.""" 49 | 50 | last_date: datetime = field(metadata=field_options(alias="lastDate")) 51 | is_private_producer: bool = field(metadata=field_options(alias="privateProducer")) 52 | device_details: Optional[list[DeviceDetails]] = field(default=None, metadata=field_options(alias="devicesDetails")) 53 | 54 | 55 | decoder = BasicDecoder(ResponseWithDescriptor[DeviceIdentity]) 56 | -------------------------------------------------------------------------------- /iec_api/models/device_type.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import Optional 3 | 4 | from mashumaro import DataClassDictMixin, field_options 5 | from mashumaro.codecs import BasicDecoder 6 | 7 | from iec_api.models.response_descriptor import ResponseWithDescriptor 8 | 9 | # 10 | # GET https://iecapi.iec.co.il//api/Device/type/{bp_number}/{contract_id}/false 11 | # 12 | # { 13 | # "data": { 14 | # "deviceNumber": "20008389", 15 | # "deviceBalance": null, 16 | # "deviceType": 0, 17 | # "estimatedDaysByWeek": null, 18 | # "averageUsageCostByWeek": null, 19 | # "estimatedDaysByMonth": null, 20 | # "averageUsageCostByMonth": null, 21 | # "balanceTime": null, 22 | # "balanceDate": null, 23 | # "isActive": true, 24 | # "numberOfDevices": 1 25 | # }, 26 | # "reponseDescriptor": { 27 | # "isSuccess": true, 28 | # "code": "00", 29 | # "description": "" 30 | # } 31 | # } 32 | # 33 | 34 | 35 | @dataclass 36 | class DeviceType(DataClassDictMixin): 37 | """Device dataclass.""" 38 | 39 | device_number: str = field(metadata=field_options(alias="deviceNumber")) 40 | device_type: int = field(metadata=field_options(alias="deviceType")) 41 | is_active: bool = field(metadata=field_options(alias="isActive")) 42 | number_of_devices: int = field(metadata=field_options(alias="numberOfDevices")) 43 | device_balance: Optional[int] = field(default=None, metadata=field_options(alias="deviceBalance")) 44 | estimated_days_by_week: Optional[int] = field(default=None, metadata=field_options(alias="estimatedDaysByWeek")) 45 | average_usage_cost_by_week: Optional[int] = field( 46 | default=None, metadata=field_options(alias="averageUsageCostByWeek") 47 | ) 48 | estimated_days_by_month: Optional[int] = field(default=None, metadata=field_options(alias="estimatedDaysByMonth")) 49 | average_usage_cost_by_month: Optional[int] = field( 50 | default=None, metadata=field_options(alias="averageUsageCostByMonth") 51 | ) 52 | balance_time: Optional[str] = field(default=None, metadata=field_options(alias="balanceTime")) 53 | balance_date: Optional[str] = field(default=None, metadata=field_options(alias="balanceDate")) 54 | 55 | 56 | decoder = BasicDecoder(ResponseWithDescriptor[DeviceType]) 57 | -------------------------------------------------------------------------------- /iec_api/models/efs.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime 3 | 4 | from mashumaro import DataClassDictMixin, field_options 5 | from mashumaro.codecs import BasicDecoder 6 | from mashumaro.config import BaseConfig 7 | 8 | from iec_api.models.response_descriptor import ResponseWithDescriptor 9 | 10 | # EFS stands for EFS service (Email, Fax, SMS) Messages 11 | # 12 | # POST https://iecapi.iec.co.il//api/customer/efs 13 | # BODY { 14 | # "contractNumber":"123456", 15 | # "processType":1, 16 | # "serviceCode": "EFS004" <- optional 17 | # } 18 | # 19 | # Response: 20 | # 21 | # { 22 | # "data": [ 23 | # { 24 | # "serviceDescription": "הודעה על הפסקה מתוכננת", 25 | # "partner": "123456", 26 | # "contractNumber": "123456", 27 | # "service": "EFS004", 28 | # "subscribeDate": "2023-06-30T00:00:00", 29 | # "subscribeTime": "0001-01-01T13:00:01", 30 | # "isActive": true, 31 | # "bpSubscription": false, 32 | # "isSms": false, 33 | # "communicationMethod": 1, 34 | # "email": "", 35 | # "sms": "055555555", 36 | # "fax": "0000000000", 37 | # "unsubscribeDate": "0001-01-01T00:00:00", 38 | # "registrationStatus": 0 39 | # } 40 | # ], 41 | # "reponseDescriptor": { 42 | # "isSuccess": true, 43 | # "code": "300", 44 | # "description": "" 45 | # } 46 | # } 47 | 48 | 49 | @dataclass 50 | class EfsRequestAllServices(DataClassDictMixin): 51 | contract_number: str = field(metadata=field_options(alias="contractNumber")) 52 | process_type: int = field(metadata=field_options(alias="processType")) 53 | 54 | class Config(BaseConfig): 55 | serialize_by_alias = True 56 | 57 | 58 | @dataclass 59 | class EfsRequestSingleService(EfsRequestAllServices): 60 | service_code: str = field(metadata=field_options(alias="serviceCode")) 61 | 62 | 63 | @dataclass 64 | class EfsMessage(DataClassDictMixin): 65 | service_description: str = field(metadata=field_options(alias="serviceDescription")) 66 | partner: str = field(metadata=field_options(alias="partner")) 67 | contract_number: str = field(metadata=field_options(alias="contractNumber")) 68 | service: str = field(metadata=field_options(alias="service")) 69 | subscribe_date: datetime = field(metadata=field_options(alias="subscribeDate")) 70 | subscribe_time: datetime = field(metadata=field_options(alias="subscribeTime")) 71 | is_active: bool = field(metadata=field_options(alias="isActive")) 72 | bp_subscription: bool = field(metadata=field_options(alias="bpSubscription")) 73 | is_sms: bool = field(metadata=field_options(alias="isSms")) 74 | communication_method: int = field(metadata=field_options(alias="communicationMethod")) 75 | email: str = field(metadata=field_options(alias="email")) 76 | sms: str = field(metadata=field_options(alias="sms")) 77 | fax: str = field(metadata=field_options(alias="fax")) 78 | unsubscribe_date: datetime = field(metadata=field_options(alias="unsubscribeDate")) 79 | registration_status: int = field(metadata=field_options(alias="registrationStatus")) 80 | 81 | 82 | decoder = BasicDecoder(ResponseWithDescriptor[list[EfsMessage]]) 83 | -------------------------------------------------------------------------------- /iec_api/models/electric_bill.py: -------------------------------------------------------------------------------- 1 | """Electric Bills.""" 2 | 3 | from dataclasses import dataclass, field 4 | from typing import Optional 5 | 6 | from mashumaro import DataClassDictMixin, field_options 7 | from mashumaro.codecs import BasicDecoder 8 | 9 | from iec_api.models.invoice import Invoice 10 | from iec_api.models.response_descriptor import ResponseWithDescriptor 11 | 12 | # GET https://iecapi.iec.co.il//api/ElectricBillsDrawers/ElectricBills/{contract_id}/{bp_number} 13 | # 14 | # { 15 | # "data": { 16 | # "totalAmountToPay": 0, 17 | # "totalInvoicesToPay": 0, 18 | # "lastDateToPay": null, 19 | # "invoices": [ 20 | # { 21 | # "fullDate": "2024-01-01T00:00:00", 22 | # "fromDate": "2023-11-15T00:00:00", 23 | # "toDate": "2024-01-17T00:00:00", 24 | # "amountOrigin": 100.00, 25 | # "amountToPay": 0, 26 | # "amountPaid": 100.00, 27 | # "invoiceId": 12345, 28 | # "contractNumber": 12345, //= contract_id 29 | # "orderNumber": 0, 30 | # "lastDate": "07/02/2024", 31 | # "invoicePaymentStatus": 1, 32 | # "documentID": "1", 33 | # "daysPeriod": 64, 34 | # "hasDirectDebit": false, 35 | # "invoiceType": 0 36 | # } 37 | # ] 38 | # }, 39 | # "reponseDescriptor": { 40 | # "isSuccess": true, 41 | # "code": null, 42 | # "description": null 43 | # } 44 | # } 45 | 46 | 47 | @dataclass 48 | class ElectricBill(DataClassDictMixin): 49 | total_amount_to_pay: float = field(metadata=field_options(alias="totalAmountToPay")) 50 | total_invoices_to_pay: int = field(metadata=field_options(alias="totalInvoicesToPay")) 51 | invoices: list[Invoice] 52 | last_date_to_pay: Optional[str] = field(metadata=field_options(alias="lastDateToPay"), default=None) 53 | 54 | 55 | decoder = BasicDecoder(ResponseWithDescriptor[ElectricBill]) 56 | -------------------------------------------------------------------------------- /iec_api/models/error_response.py: -------------------------------------------------------------------------------- 1 | """Error response object""" 2 | 3 | from dataclasses import dataclass, field 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | # GET https://iecapi.iec.co.il//api/Device/123456 8 | # {"Error": "Contract is not associated with you.", "Code": 401, "Rid": "800017a7-0003-f300-b63f-84710c7967bb"} 9 | 10 | 11 | @dataclass 12 | class IecErrorResponse(DataClassDictMixin): 13 | error: str = field(metadata=field_options(alias="Error")) 14 | code: int = field(metadata=field_options(alias="Code")) 15 | rid: str = field(metadata=field_options(alias="Rid")) 16 | -------------------------------------------------------------------------------- /iec_api/models/exceptions.py: -------------------------------------------------------------------------------- 1 | class IECError(Exception): 2 | """Exception raised for errors in the IEC API. 3 | 4 | Attributes: 5 | code -- input salary which caused the error. 6 | error -- description of the error 7 | """ 8 | 9 | def __init__(self, code, error): 10 | self.code = code 11 | self.error = error 12 | super().__init__(f"(Code {self.code}): {self.error}") 13 | 14 | 15 | class IECLoginError(IECError): 16 | """Exception raised for errors in the IEC Login. 17 | 18 | Attributes: 19 | code -- input salary which caused the error. 20 | error -- description of the error 21 | """ 22 | 23 | def __init__(self, code, error): 24 | IECError.__init__(self, code, error) 25 | -------------------------------------------------------------------------------- /iec_api/models/get_pdf.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | from mashumaro import DataClassDictMixin, field_options 4 | from mashumaro.config import BaseConfig 5 | 6 | 7 | @dataclass 8 | class GetPdfRequest(DataClassDictMixin): 9 | """ 10 | Get PDF Request dataclass. 11 | """ 12 | 13 | invoice_number: str = field(metadata=field_options(alias="invoiceNumber")) 14 | contract_id: str = field(metadata=field_options(alias="contractId")) 15 | bp_number: str = field(metadata=field_options(alias="bpNumber")) 16 | 17 | class Config(BaseConfig): 18 | serialize_by_alias = True 19 | -------------------------------------------------------------------------------- /iec_api/models/invoice.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import date, datetime 3 | from typing import Optional 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | from mashumaro.codecs import BasicDecoder 7 | 8 | from iec_api.commons import convert_to_tz_aware_datetime 9 | from iec_api.models.meter_reading import MeterReading 10 | from iec_api.models.models_commons import FormattedDate 11 | from iec_api.models.response_descriptor import ResponseWithDescriptor 12 | 13 | # GET https://iecapi.iec.co.il//api/billingCollection/invoices/{bp_number}/{contract_number} 14 | # 15 | # { 16 | # "data": { 17 | # "property": { 18 | # "areaId": "111", 19 | # "districtId": 1 20 | # }, 21 | # "invoices": [ 22 | # { 23 | # "consumption": 123.0, 24 | # "readingCode": 1, 25 | # "meterReadings": [ 26 | # { 27 | # "reading": 123, 28 | # "readingCode": null, 29 | # "readingDate": "0001-01-01T00:00:00", 30 | # "usage": null, 31 | # "serialNumber": "000000000020008389" 32 | # } 33 | # ], 34 | # "fullDate": "2020-11-01T00:00:00", 35 | # "fromDate": "2020-09-09T00:00:00", 36 | # "toDate": "2020-11-08T00:00:00", 37 | # "amountOrigin": 123.51, 38 | # "amountToPay": 0, 39 | # "amountPaid": 123.51, 40 | # "invoiceId": 123456, 41 | # "contractNumber": 123456, 42 | # "orderNumber": 0, 43 | # "lastDate": "01/12/2020", 44 | # "invoicePaymentStatus": 1, 45 | # "documentID": "1", 46 | # "daysPeriod": 61, 47 | # "hasDirectDebit": false, 48 | # "invoiceType": 0 49 | # } 50 | # ] 51 | # }, 52 | # "reponseDescriptor": { 53 | # "isSuccess": true, 54 | # "code": "00", 55 | # "description": "OK" 56 | # } 57 | # } 58 | 59 | 60 | @dataclass 61 | class Invoice(DataClassDictMixin): 62 | amount_origin: float = field(metadata=field_options(alias="amountOrigin")) 63 | amount_to_pay: float = field(metadata=field_options(alias="amountToPay")) 64 | amount_paid: float = field(metadata=field_options(alias="amountPaid")) 65 | invoice_id: int = field(metadata=field_options(alias="invoiceId")) 66 | contract_number: int = field(metadata=field_options(alias="contractNumber")) 67 | order_number: int = field(metadata=field_options(alias="orderNumber")) 68 | invoice_payment_status: int = field(metadata=field_options(alias="invoicePaymentStatus")) 69 | document_id: str = field(metadata=field_options(alias="documentID")) 70 | days_period: str = field(metadata=field_options(alias="daysPeriod")) 71 | has_direct_debit: bool = field(metadata=field_options(alias="hasDirectDebit")) 72 | invoice_type: int = field(metadata=field_options(alias="invoiceType")) 73 | 74 | reading_code: int = field(metadata=field_options(alias="readingCode"), default=0) 75 | consumption: int = field(metadata=field_options(alias="consumption"), default=0) 76 | meter_readings: list[MeterReading] = field( 77 | metadata=field_options(alias="meterReadings"), default_factory=lambda: [] 78 | ) 79 | full_date: Optional[datetime] = field(default=None, metadata=field_options(alias="fullDate")) 80 | from_date: Optional[datetime] = field(default=None, metadata=field_options(alias="fromDate")) 81 | to_date: Optional[datetime] = field(default=None, metadata=field_options(alias="toDate")) 82 | last_date: Optional[date] = field( 83 | default=None, metadata=field_options(alias="lastDate", serialization_strategy=FormattedDate("%d/%m/%Y")) 84 | ) 85 | 86 | @classmethod 87 | def __post_deserialize__(cls, obj: "Invoice") -> "Invoice": 88 | obj.full_date = convert_to_tz_aware_datetime(obj.full_date) 89 | obj.from_date = convert_to_tz_aware_datetime(obj.from_date) 90 | obj.to_date = convert_to_tz_aware_datetime(obj.to_date) 91 | return obj 92 | 93 | 94 | @dataclass 95 | class Property(DataClassDictMixin): 96 | """Property Response dataclass.""" 97 | 98 | area_id: str = field(metadata=field_options(alias="areaId")) 99 | district_id: int = field(metadata=field_options(alias="districtId")) 100 | 101 | 102 | @dataclass 103 | class GetInvoicesBody(DataClassDictMixin): 104 | """Get Invoices Response dataclass.""" 105 | 106 | property: Property 107 | invoices: list[Invoice] 108 | 109 | 110 | decoder = BasicDecoder(ResponseWithDescriptor[GetInvoicesBody]) 111 | -------------------------------------------------------------------------------- /iec_api/models/jwt.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from mashumaro import DataClassDictMixin 4 | 5 | 6 | @dataclass 7 | class JWT(DataClassDictMixin): 8 | """Okta JWT Token""" 9 | 10 | access_token: str 11 | refresh_token: str 12 | token_type: str 13 | expires_in: int 14 | scope: str 15 | id_token: str 16 | -------------------------------------------------------------------------------- /iec_api/models/meter_reading.py: -------------------------------------------------------------------------------- 1 | """Meter Reading model.""" 2 | 3 | from dataclasses import dataclass, field 4 | from datetime import datetime 5 | from typing import Optional 6 | 7 | from mashumaro import DataClassDictMixin, field_options 8 | from mashumaro.codecs import BasicDecoder 9 | 10 | from iec_api.commons import convert_to_tz_aware_datetime 11 | from iec_api.models.response_descriptor import ResponseWithDescriptor 12 | 13 | # GET https://iecapi.iec.co.il//api/Device/LastMeterReading/{contract_id}/{bp_number} 14 | # 15 | # # { 16 | # "data": { 17 | # "contractAccount": "12345", // = contract_id 18 | # "lastMeters": [ 19 | # { 20 | # "meterReadings": [ 21 | # { 22 | # "reading": 1234, 23 | # "readingCode": "01", 24 | # "readingDate": "2024-01-17T00:00:00", 25 | # "usage": "111", 26 | # "serialNumber": null 27 | # } 28 | # ], 29 | # "serialNumber": "000000000000000001", 30 | # "materialNumber": "000000000000000001", 31 | # "registerNumber": "000000000000000001" 32 | # } 33 | # ] 34 | # }, 35 | # "reponseDescriptor": { 36 | # "isSuccess": true, 37 | # "code": "00", 38 | # "description": "" 39 | # } 40 | # } 41 | 42 | 43 | @dataclass 44 | class MeterReading(DataClassDictMixin): 45 | """Meter Reading dataclass.""" 46 | 47 | reading: Optional[int] = field(default=None, metadata=field_options(alias="reading")) 48 | reading_date: Optional[datetime] = field(default=None, metadata=field_options(alias="readingDate")) 49 | serial_number: Optional[str] = field(default=None, metadata=field_options(alias="serialNumber")) 50 | reading_code: Optional[str] = field(default=None, metadata=field_options(alias="readingCode")) 51 | usage: Optional[str] = field(default=None, metadata=field_options(alias="usage")) 52 | 53 | @classmethod 54 | def __post_deserialize__(cls, obj: "MeterReading") -> "MeterReading": 55 | obj.reading_date = convert_to_tz_aware_datetime(obj.reading_date) 56 | return obj 57 | 58 | 59 | @dataclass 60 | class LastMeter(DataClassDictMixin): 61 | """Last Meters""" 62 | 63 | meter_readings: list[MeterReading] = field(metadata=field_options(alias="meterReadings")) 64 | serial_number: str = field(metadata=field_options(alias="serialNumber")) 65 | material_number: str = field(metadata=field_options(alias="materialNumber")) 66 | register_number: str = field(metadata=field_options(alias="registerNumber")) 67 | 68 | 69 | @dataclass 70 | class MeterReadings(DataClassDictMixin): 71 | """Meter Readings dataclass.""" 72 | 73 | contract_account: str = field(metadata=field_options(alias="contractAccount")) 74 | last_meters: list[LastMeter] = field(metadata=field_options(alias="lastMeters")) 75 | 76 | 77 | decoder = BasicDecoder(ResponseWithDescriptor[MeterReadings]) 78 | -------------------------------------------------------------------------------- /iec_api/models/models_commons.py: -------------------------------------------------------------------------------- 1 | from datetime import date, datetime 2 | 3 | from mashumaro.types import SerializationStrategy 4 | 5 | 6 | class FormattedDateTime(SerializationStrategy): 7 | def __init__(self, fmt): 8 | self.fmt = fmt 9 | 10 | def serialize(self, value: datetime) -> str: 11 | return value.strftime(self.fmt) 12 | 13 | def deserialize(self, value: str) -> datetime: 14 | return datetime.strptime(value, self.fmt) 15 | 16 | 17 | class FormattedDate(SerializationStrategy): 18 | def __init__(self, fmt): 19 | self.fmt = fmt 20 | 21 | def serialize(self, value: date) -> str: 22 | return value.strftime(self.fmt) 23 | 24 | def deserialize(self, value: str) -> date: 25 | return datetime.strptime(value, self.fmt).date() 26 | -------------------------------------------------------------------------------- /iec_api/models/okta_errors.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | from mashumaro import DataClassDictMixin, field_options 4 | 5 | 6 | @dataclass 7 | class OktaErrorCause(DataClassDictMixin): 8 | error_summary: str = field(metadata=field_options(alias="errorSummary")) 9 | 10 | 11 | @dataclass 12 | class OktaError(DataClassDictMixin): 13 | error_code: str = field(metadata=field_options(alias="errorCode")) 14 | error_summary: str = field(metadata=field_options(alias="errorSummary")) 15 | error_link: str = field(metadata=field_options(alias="errorLink")) 16 | error_id: str = field(metadata=field_options(alias="errorId")) 17 | error_causes: list[OktaErrorCause] = field(metadata=field_options(alias="errorCauses")) 18 | -------------------------------------------------------------------------------- /iec_api/models/outages.py: -------------------------------------------------------------------------------- 1 | """Outages model.""" 2 | 3 | from dataclasses import dataclass, field 4 | from datetime import datetime 5 | from typing import List, Optional 6 | from uuid import UUID 7 | 8 | from mashumaro import DataClassDictMixin, field_options 9 | from mashumaro.codecs import BasicDecoder 10 | 11 | from iec_api.models.response_descriptor import ResponseWithDescriptor 12 | 13 | # GET https://iecapi.iec.co.il//api/outages/transactions/{account_id}/2 14 | # 15 | # { 16 | # "data": [ 17 | # { 18 | # "transactionsInfo": [ 19 | # { 20 | # "disconnectDate": "2024-05-17T22:32:16Z", 21 | # "disconnect": { 22 | # "disconnectKey": "17701HESD5840", 23 | # "disconnectType": { 24 | # "displayName": "תקלה איזורית", 25 | # "code": 1, 26 | # "id": "b146f469-819a-ea11-a811-000d3a239ca0" 27 | # }, 28 | # "disconnectTreatmentState": { 29 | # "displayName": "החזרת אספקה", 30 | # "code": "6", 31 | # "isConnectIndicationBit": false, 32 | # "id": "7eaecdd3-859a-ea11-a812-000d3a239136" 33 | # }, 34 | # "id": "1d9a36ef-9a14-ef11-9f89-7c1e52290237", 35 | # "estimateTreatmentDate": null, 36 | # "energizedDate": "2024-05-18T11:09:41Z" 37 | # }, 38 | # "disconnectType": 1 39 | # } 40 | # ], 41 | # "site": { 42 | # "contractNumber": "346496424", 43 | # "address": { 44 | # "region": { 45 | # "name": "חיפה והצפון", 46 | # "id": "909a5d57-d7db-ea11-a813-000d3aabca53" 47 | # }, 48 | # "area": { 49 | # "name": "חיפה", 50 | # "id": "0d93fbef-d7db-ea11-a813-000d3aabca53" 51 | # }, 52 | # "street": "הגל", 53 | # "houseNumber": "12", 54 | # "city": { 55 | # "name": "טירה", 56 | # "shovalCityCode": "778", 57 | # "logicalName": null, 58 | # "id": "fb0a89b9-29e0-e911-a972-000d3a29fb7a" 59 | # }, 60 | # "id": "f5453a99-0472-e811-8106-3863bb358f68" 61 | # }, 62 | # "id": "8eb5c7da-e0a5-ea11-a812-000d3aaebb51", 63 | # "x": null, 64 | # "y": null 65 | # } 66 | # } 67 | # ], 68 | # "reponseDescriptor": { 69 | # "isSuccess": true, 70 | # "code": "0", 71 | # "description": "" 72 | # } 73 | # } 74 | 75 | # GET https://masa-faultsportalapi.iec.co.il/api/accounts/{{account_id}}/tranzactions/2 76 | # 77 | # # { 78 | # "dataCollection": [] 79 | # } 80 | 81 | 82 | @dataclass 83 | class DisconnectType(DataClassDictMixin): 84 | """DisconnectType dataclass""" 85 | 86 | display_name: str = field(metadata=field_options(alias="displayName")) 87 | code: int = field(metadata=field_options(alias="code")) 88 | id: UUID = field(metadata=field_options(alias="id")) 89 | 90 | 91 | @dataclass 92 | class DisconnectTreatmentState(DataClassDictMixin): 93 | """DisconnectTreatmentState dataclass""" 94 | 95 | display_name: str = field(metadata=field_options(alias="displayName")) 96 | code: str = field(metadata=field_options(alias="code")) 97 | is_connect_indication_bit: bool = field(metadata=field_options(alias="isConnectIndicationBit")) 98 | id: UUID = field(metadata=field_options(alias="id")) 99 | 100 | 101 | @dataclass 102 | class Disconnect(DataClassDictMixin): 103 | """Disconnect dataclass.""" 104 | 105 | id: UUID = field(metadata=field_options(alias="id")) 106 | estimate_treatment_date: Optional[datetime] = field( 107 | default=None, metadata=field_options(alias="estimateTreatmentDate") 108 | ) 109 | energized_date: Optional[str] = field(default=None, metadata=field_options(alias="energizedDate")) 110 | disconnect_key: Optional[str] = field(default=None, metadata=field_options(alias="disconnectKey")) 111 | disconnect_type: Optional[DisconnectType] = field(default=None, metadata=field_options(alias="disconnectType")) 112 | disconnect_treatment_state: Optional[DisconnectTreatmentState] = field( 113 | default=None, metadata=field_options(alias="disconnectTreatmentState") 114 | ) 115 | 116 | 117 | @dataclass 118 | class TransactionsInfo(DataClassDictMixin): 119 | """Transactions Info dataclass.""" 120 | 121 | disconnect_date: datetime = field(metadata=field_options(alias="disconnectDate")) 122 | disconnect: Disconnect = field(metadata=field_options(alias="disconnect")) 123 | disconnect_type: int = field(metadata=field_options(alias="disconnectType")) 124 | 125 | 126 | @dataclass 127 | class OutageAddress(DataClassDictMixin): 128 | """Outage Address dataclass.""" 129 | 130 | name: str = field(metadata=field_options(alias="name")) 131 | id: UUID = field(metadata=field_options(alias="id")) 132 | 133 | 134 | @dataclass 135 | class OutageAddressCity(OutageAddress): 136 | """Outage Address dataclass.""" 137 | 138 | logical_name: str = field(metadata=field_options(alias="logicalName")) 139 | shoval_city_code: str = field(metadata=field_options(alias="shovalCityCode")) 140 | 141 | 142 | @dataclass 143 | class OutageAddressFull(DataClassDictMixin): 144 | """Full Outage Address dataclass.""" 145 | 146 | region: OutageAddress = field(metadata=field_options(alias="region")) 147 | area: OutageAddress = field(metadata=field_options(alias="area")) 148 | street: str = field(metadata=field_options(alias="street")) 149 | house_number: str = field(metadata=field_options(alias="houseNumber")) 150 | city: OutageAddress = field(metadata=field_options(alias="city")) 151 | id: UUID = field(metadata=field_options(alias="id")) 152 | 153 | 154 | @dataclass 155 | class Site(DataClassDictMixin): 156 | """Site dataclass.""" 157 | 158 | contract_number: str = field(metadata=field_options(alias="contractNumber")) 159 | address: OutageAddressFull = field(metadata=field_options(alias="address")) 160 | id: UUID = field(metadata=field_options(alias="id")) 161 | x: Optional[float] = field(default=None, metadata=field_options(alias="x")) 162 | y: Optional[float] = field(default=None, metadata=field_options(alias="y")) 163 | 164 | 165 | @dataclass 166 | class Outage(DataClassDictMixin): 167 | """Outage dataclass.""" 168 | 169 | transactions_info: List[TransactionsInfo] = field(metadata=field_options(alias="transactionsInfo")) 170 | site: Site = field(metadata=field_options(alias="site")) 171 | 172 | 173 | decoder = BasicDecoder(ResponseWithDescriptor[List[Outage]]) 174 | -------------------------------------------------------------------------------- /iec_api/models/remote_reading.py: -------------------------------------------------------------------------------- 1 | """Remote Reading model.""" 2 | 3 | # Request: 4 | # curl 'https://iecapi.iec.co.il//api/Consumption/RemoteReadingRange' \ 5 | # -H 'accept: application/json, text/plain, /' \ 6 | # -H 'authorization: Bearer ' \ 7 | # -H 'content-type: application/json' \ 8 | # --data-raw '{"meterSerialNumber":"XXXXXXXX","meterCode":"123","lastInvoiceDate":"2000-01-01"\ 9 | # ,"fromDate":"2023-07-20","resolution":1}' 10 | # 11 | # Response: 12 | # { 13 | # "status": 0, 14 | # "futureConsumptionInfo": { 15 | # "lastInvoiceDate": null, 16 | # "currentDate": "2023-07-23", 17 | # "futureConsumption": 14.857, 18 | # "totalImport": 35.598, 19 | # "totalImportDate": "2023-07-23" 20 | # }, 21 | # "fromDate": "2023-07-20", 22 | # "toDate": "2023-07-20", 23 | # "totalConsumptionForPeriod": 0.325, 24 | # "totalImportDateForPeriod": "2023-07-20", 25 | # "meterStartDate": "2023-04-13", 26 | # "totalImport": 35.273, 27 | # "data": [{ "status": 8192, "date": "2023-07-20T00:00:00.000000", "value": 0 }] 28 | # } 29 | from dataclasses import dataclass, field 30 | from datetime import date, datetime 31 | from enum import IntEnum 32 | from typing import Optional 33 | 34 | from mashumaro import DataClassDictMixin, field_options 35 | from mashumaro.config import BaseConfig 36 | 37 | from iec_api.commons import convert_to_tz_aware_datetime 38 | 39 | 40 | class ReadingResolution(IntEnum): 41 | DAILY = 1 42 | WEEKLY = 2 43 | MONTHLY = 3 44 | 45 | 46 | @dataclass 47 | class RemoteReadingRequest(DataClassDictMixin): 48 | """Remote Reading Request .""" 49 | 50 | meter_serial_number: str = field(metadata=field_options(alias="meterSerialNumber")) 51 | meter_code: str = field(metadata=field_options(alias="meterCode")) 52 | from_date: str = field(metadata=field_options(alias="fromDate")) 53 | resolution: ReadingResolution = field(metadata=field_options(alias="resolution")) 54 | last_invoice_date: Optional[str] = field(default=None, metadata=field_options(alias="lastInvoiceDate")) 55 | 56 | class Config(BaseConfig): 57 | serialize_by_alias = True 58 | 59 | 60 | @dataclass 61 | class FutureConsumptionInfo(DataClassDictMixin): 62 | """Future Consumption Info dataclass.""" 63 | 64 | last_invoice_date: Optional[str] = field(default=None, metadata=field_options(alias="lastInvoiceDate")) 65 | current_date: Optional[date] = field(default=None, metadata=field_options(alias="currentDate")) 66 | future_consumption: Optional[float] = field(default=None, metadata=field_options(alias="futureConsumption")) 67 | total_import: Optional[float] = field(default=None, metadata=field_options(alias="totalImport")) 68 | total_import_date: Optional[date] = field(default=None, metadata=field_options(alias="totalImportDate")) 69 | 70 | 71 | @dataclass 72 | class RemoteReading(DataClassDictMixin): 73 | """Remote Reading Data dataclass.""" 74 | 75 | status: int 76 | date: datetime 77 | value: float 78 | 79 | def __hash__(self): 80 | """Compute the hash value the remote reading, based on all fields.""" 81 | return hash((self.status, self.date, self.value)) 82 | 83 | @classmethod 84 | def __post_deserialize__(cls, obj: "RemoteReading") -> "RemoteReading": 85 | obj.date = convert_to_tz_aware_datetime(obj.date) 86 | return obj 87 | 88 | 89 | @dataclass 90 | class RemoteReadingResponse(DataClassDictMixin): 91 | """Remote Reading Response dataclass.""" 92 | 93 | status: int 94 | future_consumption_info: FutureConsumptionInfo = field(metadata=field_options(alias="futureConsumptionInfo")) 95 | data: list[RemoteReading] 96 | from_date: Optional[date] = field(default=None, metadata=field_options(alias="fromDate")) 97 | to_date: Optional[date] = field(default=None, metadata=field_options(alias="toDate")) 98 | total_consumption_for_period: Optional[float] = field( 99 | default=None, metadata=field_options(alias="totalConsumptionForPeriod") 100 | ) 101 | total_import_date_for_period: Optional[date] = field( 102 | default=None, metadata=field_options(alias="totalImportDateForPeriod") 103 | ) 104 | meter_start_date: Optional[date] = field(default=None, metadata=field_options(alias="meterStartDate")) 105 | total_import: Optional[float] = field(default=None, metadata=field_options(alias="totalImport")) 106 | -------------------------------------------------------------------------------- /iec_api/models/response_descriptor.py: -------------------------------------------------------------------------------- 1 | """Response Descriptor""" 2 | 3 | from dataclasses import dataclass, field 4 | from typing import Generic, Optional, TypeVar 5 | 6 | from mashumaro import DataClassDictMixin, field_options 7 | 8 | 9 | @dataclass 10 | class ResponseDescriptor(DataClassDictMixin): 11 | """Response Descriptor""" 12 | 13 | is_success: bool = field(metadata=field_options(alias="isSuccess")) 14 | code: Optional[str] 15 | description: Optional[str] = None 16 | 17 | 18 | @dataclass 19 | class ErrorResponseDescriptor(DataClassDictMixin): 20 | """Error Response Descriptor""" 21 | 22 | error: str = field(metadata=field_options(alias="Error")) 23 | code: int = field(metadata=field_options(alias="Code")) 24 | rid: str = field(metadata=field_options(alias="Rid")) 25 | 26 | 27 | RESPONSE_DESCRIPTOR_FIELD = "reponseDescriptor" 28 | 29 | 30 | T = TypeVar("T") 31 | 32 | 33 | @dataclass 34 | class ResponseWithDescriptor(Generic[T], DataClassDictMixin): 35 | """Response With Descriptor""" 36 | 37 | response_descriptor: ResponseDescriptor = field(metadata=field_options(alias=RESPONSE_DESCRIPTOR_FIELD)) 38 | data: Optional[T] = None 39 | -------------------------------------------------------------------------------- /iec_api/models/send_consumption_to_mail.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | from mashumaro import DataClassDictMixin, field_options 4 | from mashumaro.config import BaseConfig 5 | 6 | # POST https://iecapi.iec.co.il/api/Consumption/SendConsumptionReportToMail/{{contract_id}} 7 | # { 8 | # "emailAddress": "sefi.ninio@gmail.com", 9 | # "meterCode": "{{device_code}}", 10 | # "meterSerial": "{{device_id}}" 11 | # } 12 | # 13 | # 14 | # True 15 | # 16 | 17 | 18 | @dataclass 19 | class SendConsumptionReportToMailRequest(DataClassDictMixin): 20 | """ 21 | Send Consumption Report To Mail Request dataclass. 22 | """ 23 | 24 | email_address: str = field(metadata=field_options(alias="emailAddress")) 25 | meter_code: str = field(metadata=field_options(alias="meterCode")) 26 | meter_serial: str = field(metadata=field_options(alias="meterSerial")) 27 | 28 | class Config(BaseConfig): 29 | serialize_by_alias = True 30 | -------------------------------------------------------------------------------- /iec_api/static_data.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from decimal import Decimal 3 | 4 | from aiohttp import ClientSession 5 | 6 | from iec_api import commons 7 | from iec_api.const import GET_KWH_TARIFF_URL, GET_PREIOD_CALCULATOR_URL 8 | from iec_api.usage_calculator.calculator import UsageCalculator 9 | 10 | usage_calculator = UsageCalculator() 11 | cache = {} 12 | distribution_1p_tariff_key = "distribution_1p_tariff" 13 | distribution_3p_tariff_key = "distribution_3p_tariff" 14 | delivery_1p_tariff_key = "delivery_1p_tariff" 15 | delivery_3p_tariff_key = "delivery_3p_tariff" 16 | kwh_tariff_key = "kwh_tariff" 17 | kva_tariff_key = "kva_tariff" 18 | connection_to_power_size_key = "connection_to_power_size" 19 | vat_key = "vat" 20 | 21 | 22 | async def get_usage_calculator(session: ClientSession) -> UsageCalculator: 23 | """Get Usage Calculator from IEC API data.""" 24 | 25 | if not usage_calculator.is_loaded: 26 | await usage_calculator.load_data(session) 27 | 28 | return usage_calculator 29 | 30 | 31 | async def get_kwh_tariff(session: ClientSession) -> float: 32 | if kwh_tariff_key not in cache: 33 | calculator = await get_usage_calculator(session) 34 | kwh_tariff = calculator.get_kwh_tariff() 35 | cache[kwh_tariff_key] = kwh_tariff 36 | 37 | return cache[kwh_tariff_key] 38 | 39 | 40 | async def _get_tariffs(session: ClientSession) -> tuple[float, float, float, float, float]: 41 | """Get Device Type data response from IEC API.""" 42 | response = await commons.send_get_request(session=session, url=GET_KWH_TARIFF_URL) 43 | kwh_tariff_str = response["components"][1]["table"][1][2]["value"] 44 | kwh_tariff = float(base64.b64decode(kwh_tariff_str).decode("utf-8")) 45 | 46 | distribution_1p_tariff_str = response["components"][2]["table"][1][2]["value"] 47 | distribution_1p_tariff = float(base64.b64decode(distribution_1p_tariff_str).decode("utf-8")) 48 | distribution_3p_tariff_str = response["components"][2]["table"][2][2]["value"] 49 | distribution_3p_tariff = float(base64.b64decode(distribution_3p_tariff_str).decode("utf-8")) 50 | 51 | delivery_1p_tariff_str = response["components"][3]["table"][1][2]["value"] 52 | delivery_3p_tariff_str = response["components"][3]["table"][2][2]["value"] 53 | delivery_1p_tariff = float(base64.b64decode(delivery_1p_tariff_str).decode("utf-8")) 54 | delivery_3p_tariff = float(base64.b64decode(delivery_3p_tariff_str).decode("utf-8")) 55 | 56 | kva_tariff_str = response["components"][5]["table"][1][2]["value"] 57 | kva_tariff = float(base64.b64decode(kva_tariff_str).decode("utf-8")) 58 | 59 | cache[distribution_1p_tariff_key] = distribution_1p_tariff 60 | cache[distribution_3p_tariff_key] = distribution_3p_tariff 61 | cache[delivery_1p_tariff_key] = delivery_1p_tariff 62 | cache[delivery_3p_tariff_key] = delivery_3p_tariff 63 | cache[kva_tariff_key] = kva_tariff 64 | 65 | return kwh_tariff, distribution_1p_tariff, distribution_3p_tariff, delivery_1p_tariff, delivery_3p_tariff 66 | 67 | 68 | async def get_distribution_tariff(session: ClientSession, phase_count: int) -> float: 69 | """Get distribution tariff (incl. VAT) from IEC API.""" 70 | 71 | key = distribution_3p_tariff_key if phase_count == 3 else distribution_1p_tariff_key 72 | if key not in cache: 73 | await _get_tariffs(session) 74 | 75 | return cache[key] 76 | 77 | 78 | async def get_delivery_tariff(session: ClientSession, phase_count: int) -> float: 79 | """Get delivery tariff (incl. VAT) from IEC API.""" 80 | 81 | key = delivery_3p_tariff_key if phase_count == 3 else delivery_1p_tariff_key 82 | if key not in cache: 83 | await _get_tariffs(session) 84 | 85 | return cache[key] 86 | 87 | 88 | async def get_kva_tariff(session: ClientSession) -> float: 89 | """Get KVA tariff (incl. VAT) from IEC API.""" 90 | 91 | key = kva_tariff_key 92 | if key not in cache: 93 | await _get_tariffs(session) 94 | 95 | return cache[key] 96 | 97 | 98 | async def _get_vat(session: ClientSession) -> Decimal: 99 | """Get VAT from IEC API.""" 100 | 101 | key = vat_key 102 | if key not in cache: 103 | calculator = await get_usage_calculator(session) 104 | cache[key] = calculator.get_vat() 105 | 106 | return cache[key] 107 | 108 | 109 | async def _get_connection_to_power_size(session: ClientSession) -> dict[str, float]: 110 | """Get Device Type data response from IEC API.""" 111 | resp = await commons.send_get_request(session=session, url=GET_PREIOD_CALCULATOR_URL) 112 | connection_to_power_size_map = resp["period_Calculator_Rates"]["connectionToPowerSize"] 113 | 114 | cache[connection_to_power_size_key] = connection_to_power_size_map 115 | return connection_to_power_size_map 116 | 117 | 118 | async def get_power_size(session: ClientSession, connection: str) -> float: 119 | """Get PowerSize by Connection (incl. VAT) from IEC API.""" 120 | 121 | key = connection_to_power_size_key 122 | if key not in cache: 123 | await _get_connection_to_power_size(session) 124 | 125 | connection_to_power_size_map = cache[key] 126 | 127 | # If connection is not found, return 0 128 | power_size = connection_to_power_size_map.get(connection, 0) 129 | 130 | vat = await _get_vat(session) 131 | return round(power_size * (1 + float(vat)), 2) 132 | -------------------------------------------------------------------------------- /iec_api/usage_calculator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/py-iec-api/0f71f1cb7c61870cb34eccf2ed7f0d8d1d6f2635/iec_api/usage_calculator/__init__.py -------------------------------------------------------------------------------- /iec_api/usage_calculator/calculator.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import timedelta 3 | from typing import Optional 4 | 5 | from aiohttp import ClientSession 6 | 7 | from iec_api import commons 8 | from iec_api.const import GET_CALCULATOR_GADGET_URL 9 | from iec_api.usage_calculator.consumption import Consumption 10 | from iec_api.usage_calculator.electric_device import ElectricDevice, PowerUnit 11 | from iec_api.usage_calculator.get_calculator_response import GetCalculatorResponse 12 | from iec_api.usage_calculator.rates import Rates 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class UsageCalculator: 18 | """Usage Calculator""" 19 | 20 | def __init__(self): 21 | self.devices: list[ElectricDevice] = [] 22 | self.rates: Rates | None = None 23 | self.is_loaded = False 24 | 25 | async def load_data(self, session: ClientSession): 26 | if not self.is_loaded: 27 | iec_api_data = await commons.send_get_request(session=session, url=GET_CALCULATOR_GADGET_URL) 28 | response = GetCalculatorResponse.from_dict(iec_api_data) 29 | self.devices: list[ElectricDevice] = response.electric_devices 30 | self.rates: Rates | None = response.gadget_calculator_rates 31 | self.is_loaded = True 32 | else: 33 | logger.info("Usage calculator data was already loaded") 34 | 35 | def get_vat(self) -> float: 36 | if not self.is_loaded: 37 | raise ValueError("Usage calculator data is not loaded") 38 | return self.rates.vat / 100 39 | 40 | def get_kwh_tariff(self) -> float: 41 | if not self.is_loaded: 42 | raise ValueError("Usage calculator data is not loaded") 43 | return float(self.rates.home_rate * (1 + self.rates.vat / 100)) 44 | 45 | def get_device_names(self) -> list[str]: 46 | if not self.is_loaded: 47 | raise ValueError("Usage calculator data is not loaded") 48 | return list(map(lambda device: device.name, self.devices)) 49 | 50 | def get_device_info_by_name(self, name: str) -> Optional[ElectricDevice]: 51 | if not self.is_loaded: 52 | raise ValueError("Usage calculator data is not loaded") 53 | for device in self.devices: 54 | if device.name == name: 55 | return device 56 | return None 57 | 58 | def get_consumption_by_device_and_time( 59 | self, name: str, time_delta: timedelta, custom_usage_value: Optional[float] 60 | ) -> Optional[Consumption]: 61 | device = self.get_device_info_by_name(name) 62 | if not device: 63 | return None 64 | 65 | minutes = time_delta.total_seconds() / 60 66 | 67 | consumption = self._convert_to_kwh(device, custom_usage_value) 68 | rate = float(self.rates.home_rate * (1 + self.rates.vat / 100)) 69 | 70 | return Consumption( 71 | name=name, 72 | power=custom_usage_value if custom_usage_value else device.power, 73 | power_unit=device.power_unit, 74 | consumption=consumption, 75 | cost=consumption * rate, 76 | duration=timedelta(minutes=minutes), 77 | ) 78 | 79 | @staticmethod 80 | def _convert_to_kwh(device: ElectricDevice, custom_usage_value: Optional[float] = None) -> float: 81 | # From IEC Logic 82 | 83 | power = custom_usage_value if custom_usage_value else device.power 84 | 85 | match device.power_unit: 86 | case PowerUnit.KiloWatt: 87 | return power 88 | case PowerUnit.Watt: 89 | return power * 0.001 90 | case PowerUnit.HorsePower: 91 | return power * 0.736 92 | case PowerUnit.Ampere: 93 | return power * 0.23 94 | -------------------------------------------------------------------------------- /iec_api/usage_calculator/consumption.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import timedelta 3 | from decimal import Decimal 4 | 5 | from iec_api.usage_calculator.electric_device import PowerUnit 6 | 7 | 8 | @dataclass 9 | class Consumption: 10 | """Consumption dataclass.""" 11 | 12 | name: str 13 | power: float 14 | power_unit: PowerUnit 15 | duration: timedelta 16 | consumption: float 17 | cost: Decimal 18 | -------------------------------------------------------------------------------- /iec_api/usage_calculator/electric_device.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from enum import IntEnum 3 | 4 | from mashumaro import DataClassDictMixin, field_options 5 | 6 | 7 | class CalculationResolution(IntEnum): 8 | """Calculation Resolution enum.""" 9 | 10 | MINUTE = 1 11 | HOUR = 2 12 | 13 | 14 | class PowerUnit(IntEnum): 15 | """Power Unit enum.""" 16 | 17 | KiloWatt = 1 18 | Watt = 2 19 | HorsePower = 3 20 | Ampere = 4 21 | 22 | 23 | @dataclass 24 | class ElectricDevice(DataClassDictMixin): 25 | """Electric Device dataclass.""" 26 | 27 | name: str = field(metadata=field_options(alias="name")) 28 | calculation_resolution: CalculationResolution = field(metadata=field_options(alias="calculationResolution")) 29 | power: int = field(metadata=field_options(alias="power")) 30 | power_unit: PowerUnit = field(metadata=field_options(alias="powerUnit")) 31 | average_duration_time_of_operation_in_minutes: float = field( 32 | metadata=field_options(alias="avarageDurationTimeOfOperationInMinutes") 33 | ) 34 | -------------------------------------------------------------------------------- /iec_api/usage_calculator/get_calculator_response.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | from mashumaro import DataClassDictMixin, field_options 4 | 5 | from iec_api.usage_calculator.electric_device import ElectricDevice 6 | from iec_api.usage_calculator.rates import Rates 7 | 8 | 9 | @dataclass 10 | class GetCalculatorResponse(DataClassDictMixin): 11 | """Calculator Rates dataclass.""" 12 | 13 | gadget_calculator_rates: Rates = field(metadata=field_options(alias="gadget_Calculator_Rates")) 14 | electric_devices: list[ElectricDevice] = field(metadata=field_options(alias="electric_Devices")) 15 | -------------------------------------------------------------------------------- /iec_api/usage_calculator/rates.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime 3 | from decimal import Decimal 4 | 5 | from mashumaro import DataClassDictMixin, field_options 6 | 7 | 8 | @dataclass 9 | class Rates(DataClassDictMixin): 10 | """Calculator Rates dataclass.""" 11 | 12 | last_updated: datetime = field(metadata=field_options(alias="lastUpdated")) 13 | home_rate: Decimal = field(metadata=field_options(alias="homeRate")) 14 | general_rate: Decimal = field(metadata=field_options(alias="generalRate")) 15 | vat: Decimal = field(metadata=field_options(alias="vat")) 16 | -------------------------------------------------------------------------------- /logging.conf: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root,app 3 | 4 | [handlers] 5 | keys=consoleHandler,detailedConsoleHandler,jsonConsoleHandler 6 | 7 | [formatters] 8 | keys=normalFormatter,detailedFormatter,jsonFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=jsonConsoleHandler 13 | 14 | [logger_app] 15 | level=INFO 16 | handlers=jsonConsoleHandler 17 | qualname=app 18 | propagate=0 19 | 20 | [handler_consoleHandler] 21 | class=StreamHandler 22 | level=INFO 23 | formatter=normalFormatter 24 | args=(sys.stdout,) 25 | 26 | [handler_detailedConsoleHandler] 27 | class=StreamHandler 28 | level=INFO 29 | formatter=detailedFormatter 30 | args=(sys.stdout,) 31 | 32 | [handler_jsonConsoleHandler] 33 | class=StreamHandler 34 | level=INFO 35 | formatter=jsonFormatter 36 | args=(sys.stdout,) 37 | 38 | [formatter_normalFormatter] 39 | format=%(asctime)s loglevel=%(levelname)-6s logger=%(name)s %(funcName)s() L%(lineno)-4d %(message)s 40 | 41 | [formatter_detailedFormatter] 42 | format=%(asctime)s loglevel=%(levelname)-6s logger=%(name)s %(funcName)s() L%(lineno)-4d %(message)s call_trace=%(pathname)s L%(lineno)-4d 43 | 44 | [formatter_jsonFormatter] 45 | format="{'time':'%(asctime)s', 'logger': '%(name)s', 'level': '%(levelname)s', 'message': '%(message)s'}" -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "iec-api" 3 | version = "0.0.1" 4 | description = "A Python wrapper for Israel Electric Company API" 5 | authors = ["GuyKh"] 6 | license = "MIT" 7 | readme = "README.md" 8 | maintainers = [ 9 | "Guy Khmelnitsky ", 10 | ] 11 | repository = "https://github.com/GuyKh/py-iec-api" 12 | keywords = ["python", "poetry", "api", "iec", "israel", "electric"] 13 | packages = [ 14 | { include = "iec_api" } 15 | ] 16 | 17 | [tool.poetry.dependencies] 18 | python = "^3.10" 19 | mashumaro = "^3.13" 20 | pyjwt = "^2.8.0" 21 | requests = "^2.31.0" 22 | pkce = "^1.0.3" 23 | aiohttp = "^3.9.1" 24 | aiofiles = ">=23.2.1,<25.0.0" 25 | pytz = "^2024.1" 26 | 27 | [tool.poetry.group.dev.dependencies] 28 | pytest = "8.4.1" 29 | pytest-cov = "^6.0.0" 30 | ruff = "^0.12.0" 31 | pre-commit = "^4.0.0" 32 | 33 | [tool.pytest.ini_options] 34 | testpaths = ["tests",] 35 | 36 | [tool.coverage.run] 37 | branch = true 38 | omit = ["*/tests/*", "example.py"] 39 | 40 | [tool.coverage.report] 41 | show_missing = true 42 | fail_under = 100 43 | 44 | [tool.coverage.html] 45 | directory = "htmlcov" 46 | 47 | [tool.ruff] 48 | line-length = 120 49 | lint.select = ["E", "F", "W", "I", "N"] 50 | target-version = "py311" 51 | 52 | [build-system] 53 | requires = ["poetry-core>=1.0.0"] 54 | build-backend = "poetry.core.masonry.api" 55 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GuyKh/py-iec-api/0f71f1cb7c61870cb34eccf2ed7f0d8d1d6f2635/tests/__init__.py -------------------------------------------------------------------------------- /tests/commons_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import iec_api.commons 4 | 5 | 6 | class CommonsTest(unittest.TestCase): 7 | def test_valid_israeli_id(self): 8 | user_id = 123456782 9 | self.assertTrue(iec_api.commons.is_valid_israeli_id(user_id), "Israeli ID should be valid") 10 | 11 | def test_invalid_israeli_id(self): 12 | user_id = 123456789 13 | self.assertFalse(iec_api.commons.is_valid_israeli_id(user_id), "Israeli ID should be invalid") 14 | 15 | def test_invalid_israeli_id_long(self): 16 | user_id = 1234567890 17 | self.assertFalse(iec_api.commons.is_valid_israeli_id(user_id), "Israeli ID should be invalid") 18 | 19 | 20 | if __name__ == "__main__": 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /tests/e2e_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime, timedelta 3 | 4 | from iec_api.iec_client import IecClient 5 | from iec_api.models.jwt import JWT 6 | 7 | 8 | class CommonsTest(unittest.IsolatedAsyncioTestCase): 9 | jwt_token = { 10 | "access_token": "Fill", 11 | "refresh_token": "this", 12 | "token_type": "Bearer", 13 | "expires_in": 3600, 14 | "scope": "offline_access email openid profile", 15 | "id_token": "yourself", 16 | } 17 | 18 | async def test_e2e_with_existing_token(self): 19 | user_id = 1234567832 20 | 21 | client = IecClient(user_id) 22 | await client.load_jwt_token(JWT.from_dict(self.jwt_token)) 23 | await client.check_token() 24 | await client.refresh_token() 25 | await client.save_token_to_file() 26 | 27 | await client.get_customer() 28 | await client.get_accounts() 29 | await client.get_default_account() 30 | await client.get_contracts() 31 | await client.get_default_contract() 32 | 33 | await client.get_device_type() 34 | devices = await client.get_devices() 35 | device = devices[0] 36 | await client.get_device_by_device_id(device_id=device.device_number) 37 | 38 | await client.get_billing_invoices() 39 | await client.get_electric_bill() 40 | await client.get_last_meter_reading() 41 | 42 | selected_date: datetime = datetime.now() - timedelta(days=30) 43 | 44 | await client.get_remote_reading(device.device_number, int(device.device_code), selected_date, selected_date) 45 | 46 | await client.save_token_to_file() 47 | await client.load_token_from_file() 48 | await client.check_token() 49 | await client.refresh_token() 50 | 51 | 52 | if __name__ == "__main__": 53 | unittest.main() 54 | --------------------------------------------------------------------------------