├── .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 [](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 |
--------------------------------------------------------------------------------