├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github ├── release-drafter.yml ├── renovate.json └── workflows │ ├── draft-release.yml │ ├── format.yml │ ├── lint.yml │ ├── release.yml │ ├── test.yml │ └── type.yml ├── .gitignore ├── .prettierignore ├── LICENSE ├── PyViCare ├── Feature.py ├── PyViCare.py ├── PyViCareAbstractOAuthManager.py ├── PyViCareBrowserOAuthManager.py ├── PyViCareCachedService.py ├── PyViCareDevice.py ├── PyViCareDeviceConfig.py ├── PyViCareElectricalEnergySystem.py ├── PyViCareFuelCell.py ├── PyViCareGateway.py ├── PyViCareGazBoiler.py ├── PyViCareHeatCurveCalculation.py ├── PyViCareHeatPump.py ├── PyViCareHeatingDevice.py ├── PyViCareHybrid.py ├── PyViCareOAuthManager.py ├── PyViCareOilBoiler.py ├── PyViCarePelletsBoiler.py ├── PyViCareRadiatorActuator.py ├── PyViCareRoomSensor.py ├── PyViCareService.py ├── PyViCareUtils.py ├── PyViCareVentilationDevice.py └── __init__.py ├── README.md ├── mypy.ini ├── poetry.lock ├── pyproject.toml └── tests ├── ViCareServiceMock.py ├── __init__.py ├── helper.py ├── response ├── Ecotronic.json ├── Solar.json ├── TCU300_ethernet.json ├── VitoairFs300E.json ├── Vitocal-200S-with-Vitovent-300W.json ├── Vitocal111S.json ├── Vitocal151A.json ├── Vitocal200.json ├── Vitocal200S.json ├── Vitocal200S_E8NEV.json ├── Vitocal222S.json ├── Vitocal250A.json ├── Vitocal252.json ├── Vitocal300G.json ├── Vitocal333G.json ├── Vitocaldens222F.json ├── Vitocharge05.json ├── VitochargeVX3.json ├── VitoconnectOpto1.json ├── VitoconnectOpto2.json ├── Vitodens050W.json ├── Vitodens100NA.json ├── Vitodens100W.json ├── Vitodens100W_B1HC-26.json ├── Vitodens111W.json ├── Vitodens200W.json ├── Vitodens200W_2.json ├── Vitodens200W_B2HA.json ├── Vitodens222W.json ├── Vitodens300W.json ├── Vitodens333F.json ├── Vitodens343F_B3UF.json ├── Vitodens_100_BHC_0421.json ├── VitolaUniferral.json ├── Vitopure350.json ├── VitovalorPT2.json ├── deviceerrors │ └── F.1100.json ├── errors │ ├── error_500.json │ ├── error_502.json │ ├── expired_token.json │ ├── gateway_offline.json │ └── rate_limit.json ├── zigbee_Smart_Device_eTRV_generic_50.json ├── zigbee_Smart_cs_generic_50.json ├── zigbee_zk03839.json ├── zigbee_zk03840_trv.json └── zigbee_zk05390_repeater.json ├── test_E3_TCU300_ethernet.py ├── test_Ecotronic.py ├── test_GenericDevice.py ├── test_Integration.py ├── test_PyViCareCachedService.py ├── test_PyViCareDeviceConfig.py ├── test_PyViCareExceptions.py ├── test_PyViCareService.py ├── test_Solar.py ├── test_TestForMissingProperties.py ├── test_Utils.py ├── test_ViCareOAuthManager.py ├── test_VitoairFs300E.py ├── test_Vitocal111S.py ├── test_Vitocal151A.py ├── test_Vitocal200.py ├── test_Vitocal200S.py ├── test_Vitocal222S.py ├── test_Vitocal250A.py ├── test_Vitocal300G.py ├── test_Vitocal333G.py ├── test_Vitocaldens222F.py ├── test_Vitocharge05.py ├── test_VitochargeVX3.py ├── test_VitoconnectOpto1.py ├── test_VitoconnectOpto2.py ├── test_Vitodens100W.py ├── test_Vitodens200W.py ├── test_Vitodens200W_2.py ├── test_Vitodens222W.py ├── test_Vitodens300W.py ├── test_Vitodens333F.py ├── test_VitolaUniferral.py ├── test_Vitopure350.py ├── test_VitovalorPT2.py ├── test_device_error.py ├── test_vitocal-with-vitovent.py ├── test_zigbee_cs.py ├── test_zigbee_eTRV.py ├── test_zigbee_zk03839.py └── test_zigbee_zk03840.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.202.3/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster 4 | ARG VARIANT="3.9-bullseye" 5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 6 | 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="none" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 12 | # COPY requirements.txt /tmp/pip-tmp/ 13 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 14 | # && rm -rf /tmp/pip-tmp 15 | 16 | # [Optional] Uncomment this section to install additional OS packages. 17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | # && apt-get -y install --no-install-recommends 19 | 20 | # [Optional] Uncomment this line to install global node packages. 21 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.202.3/containers/python-3 3 | { 4 | "name": "Python 3", 5 | "runArgs": [ 6 | "--init" 7 | ], 8 | "build": { 9 | "dockerfile": "Dockerfile", 10 | "context": "..", 11 | "args": { 12 | // Update 'VARIANT' to pick a Python version: 3, 3.9, 3.8, 3.7, 3.6. 13 | // Append -bullseye or -buster to pin to an OS version. 14 | // Use -bullseye variants on local on arm64/Apple Silicon. 15 | "VARIANT": "3.9", 16 | // Options 17 | "NODE_VERSION": "16" 18 | } 19 | }, 20 | // Set *default* container specific settings.json values on container create. 21 | "settings": { 22 | "files.insertFinalNewline": true, 23 | "editor.insertSpaces": true, 24 | "editor.tabSize": 4, 25 | "editor.formatOnSave": true, 26 | "python.pythonPath": "/usr/local/bin/python", 27 | "python.languageServer": "Pylance", 28 | "python.linting.enabled": true, 29 | "python.linting.pylintEnabled": false, 30 | "python.linting.flake8Enabled": true, 31 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 32 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 33 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 34 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 35 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 36 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 37 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 38 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 39 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" 40 | }, 41 | // Add the IDs of extensions you want installed when the container is created. 42 | "extensions": [ 43 | "ms-python.python", 44 | "ms-python.vscode-pylance" 45 | ], 46 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 47 | // "forwardPorts": [], 48 | // Use 'postCreateCommand' to run commands after the container is created. 49 | "postCreateCommand": "pip3 install --user -r requirements.txt && npm config set update-notifier false", 50 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 51 | "remoteUser": "vscode", 52 | "features": { 53 | "github-cli": "latest" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.py] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.json] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [Makefile] 20 | indent_style = tab 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: "$RESOLVED_VERSION" 3 | tag-template: "$RESOLVED_VERSION" 4 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 5 | sort-direction: ascending 6 | 7 | categories: 8 | - title: "🚨 Breaking changes" 9 | labels: 10 | - "breaking-change" 11 | - title: "✨ New features" 12 | labels: 13 | - "new-feature" 14 | - title: "🐛 Bug fixes" 15 | labels: 16 | - "bugfix" 17 | - title: "🚀 Enhancements" 18 | labels: 19 | - "enhancement" 20 | - "refactor" 21 | - "performance" 22 | - title: "🧰 Maintenance" 23 | labels: 24 | - "maintenance" 25 | - "ci" 26 | - title: "📚 Documentation" 27 | labels: 28 | - "documentation" 29 | - title: "⬆️ Dependency updates" 30 | labels: 31 | - "dependencies" 32 | 33 | version-resolver: 34 | major: 35 | labels: 36 | - "major" 37 | - "breaking-change" 38 | minor: 39 | labels: 40 | - "minor" 41 | - "new-feature" 42 | patch: 43 | labels: 44 | - "bugfix" 45 | - "chore" 46 | - "ci" 47 | - "dependencies" 48 | - "documentation" 49 | - "enhancement" 50 | - "performance" 51 | - "refactor" 52 | default: patch 53 | 54 | template: | 55 | ## What’s changed 56 | 57 | $CHANGES 58 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "labels": ["dependencies"], 7 | "packageRules": [ 8 | { 9 | "automerge": true, 10 | "platformAutomerge": true, 11 | "labels": ["maintenance"], 12 | "matchPackageNames": [ 13 | "ruff", 14 | "mypy", 15 | "pylint", 16 | "codespell" 17 | ], 18 | "description": "Automerge check tools" 19 | }, 20 | { 21 | "matchDepTypes": ["devDependencies"], 22 | "labels": ["maintenance"] 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | push: 6 | branches: 7 | - master 8 | workflow_dispatch: 9 | 10 | jobs: 11 | update_release_draft: 12 | name: ✏️ Draft release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | pull-requests: read 17 | steps: 18 | - name: 🚀 Run Release Drafter 19 | uses: release-drafter/release-drafter@v6.1.0 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | env: 13 | DEFAULT_PYTHON: "3.11" 14 | 15 | jobs: 16 | sort: 17 | name: sort testdata 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: ⤵️ Check out code from GitHub 21 | uses: actions/checkout@v4.2.2 22 | - name: 🚀 Sort test response data 23 | run: | 24 | find './tests/response' \ 25 | -maxdepth '1' \ 26 | -type 'f' \ 27 | -name '*.json' \ 28 | -exec sh -c 'mv $1 $1.tmp; jq ".data|=sort_by(.feature)" --sort-keys $1.tmp > $1; rm $1.tmp' shell {} ";" 29 | - name: 🔍 Verify 30 | run: git diff --name-only --exit-code 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | workflow_dispatch: 10 | env: 11 | DEFAULT_PYTHON: "3.11" 12 | jobs: 13 | codespell: 14 | name: codespell 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: ⤵️ Check out code from GitHub 18 | uses: actions/checkout@v4.2.2 19 | - name: 🏗 Set up Poetry 20 | run: pipx install poetry 21 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 22 | id: python 23 | uses: actions/setup-python@v5.6.0 24 | with: 25 | python-version: ${{ env.DEFAULT_PYTHON }} 26 | cache: "poetry" 27 | - name: 🏗 Install workflow dependencies 28 | run: | 29 | poetry config virtualenvs.create true 30 | poetry config virtualenvs.in-project true 31 | - name: 🏗 Install Python dependencies 32 | run: poetry install --no-interaction 33 | - name: 🚀 Check code for common misspellings 34 | run: poetry run codespell **/*.py 35 | 36 | ruff: 37 | name: ruff 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: ⤵️ Check out code from GitHub 41 | uses: actions/checkout@v4.2.2 42 | - name: 🏗 Set up Poetry 43 | run: pipx install poetry 44 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 45 | id: python 46 | uses: actions/setup-python@v5.6.0 47 | with: 48 | python-version: ${{ env.DEFAULT_PYTHON }} 49 | cache: "poetry" 50 | - name: 🏗 Install workflow dependencies 51 | run: | 52 | poetry config virtualenvs.create true 53 | poetry config virtualenvs.in-project true 54 | - name: 🏗 Install Python dependencies 55 | run: poetry install --no-interaction 56 | - name: 🚀 Run ruff linter 57 | run: poetry run ruff check --output-format=github . 58 | # - name: 🚀 Run ruff formatter 59 | # run: poetry run ruff format --check . 60 | 61 | pylint: 62 | name: pylint 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: ⤵️ Check out code from GitHub 66 | uses: actions/checkout@v4.2.2 67 | - name: 🏗 Set up Poetry 68 | run: pipx install poetry 69 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 70 | id: python 71 | uses: actions/setup-python@v5.6.0 72 | with: 73 | python-version: ${{ env.DEFAULT_PYTHON }} 74 | cache: "poetry" 75 | - name: 🏗 Install workflow dependencies 76 | run: | 77 | poetry config virtualenvs.create true 78 | poetry config virtualenvs.in-project true 79 | - name: 🏗 Install Python dependencies 80 | run: poetry install --no-interaction 81 | - name: 🚀 Run pylint 82 | run: poetry run pylint **/*.py 83 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | release: 6 | types: 7 | - published 8 | 9 | env: 10 | DEFAULT_PYTHON: "3.11" 11 | 12 | jobs: 13 | release: 14 | name: Releasing to PyPI 15 | runs-on: ubuntu-latest 16 | environment: 17 | name: PyPI 18 | url: https://pypi.org/p/PyViCare 19 | permissions: 20 | contents: write 21 | id-token: write 22 | steps: 23 | - name: ⤵️ Check out code from GitHub 24 | uses: actions/checkout@v4.2.2 25 | - name: 🏗 Set up Poetry 26 | run: pipx install poetry 27 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 28 | id: python 29 | uses: actions/setup-python@v5.6.0 30 | with: 31 | python-version: ${{ env.DEFAULT_PYTHON }} 32 | cache: "poetry" 33 | - name: 🏗 Install workflow dependencies 34 | run: | 35 | poetry config virtualenvs.create true 36 | poetry config virtualenvs.in-project true 37 | - name: 🏗 Install dependencies 38 | run: poetry install --no-interaction 39 | - name: 🏗 Set package version 40 | run: | 41 | version="${{ github.event.release.tag_name }}" 42 | version="${version,,}" 43 | version="${version#v}" 44 | poetry version --no-interaction "${version}" 45 | - name: 🏗 Build package 46 | run: poetry build --no-interaction 47 | - name: 🚀 Publish to PyPi 48 | uses: pypa/gh-action-pypi-publish@v1.12.4 49 | with: 50 | verbose: true 51 | print-hash: true 52 | password: ${{ secrets.PYPI_APITOKEN }} 53 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | env: 13 | DEFAULT_PYTHON: "3.11" 14 | 15 | jobs: 16 | pytest: 17 | name: Python ${{ matrix.python }} 18 | runs-on: ubuntu-latest 19 | strategy: 20 | matrix: 21 | python: ["3.9", "3.10", "3.11", "3.12"] 22 | steps: 23 | - name: ⤵️ Check out code from GitHub 24 | uses: actions/checkout@v4.2.2 25 | - name: 🏗 Set up Poetry 26 | run: pipx install poetry 27 | - name: 🏗 Set up Python ${{ matrix.python }} 28 | id: python 29 | uses: actions/setup-python@v5.6.0 30 | with: 31 | python-version: ${{ matrix.python }} 32 | cache: "poetry" 33 | - name: 🏗 Install workflow dependencies 34 | run: | 35 | poetry config virtualenvs.create true 36 | poetry config virtualenvs.in-project true 37 | - name: 🏗 Install dependencies 38 | run: poetry install --no-interaction 39 | - name: 🚀 Run pytest 40 | run: poetry run pytest --cov PyViCare 41 | - name: ⬆️ Upload coverage artifact 42 | uses: actions/upload-artifact@v4.6.2 43 | with: 44 | name: coverage-${{ matrix.python }} 45 | path: .coverage 46 | -------------------------------------------------------------------------------- /.github/workflows/type.yml: -------------------------------------------------------------------------------- 1 | name: Type 2 | 3 | # yamllint disable-line rule:truthy 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | env: 13 | DEFAULT_PYTHON: "3.11" 14 | 15 | jobs: 16 | mypy: 17 | name: mypy 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: ⤵️ Check out code from GitHub 21 | uses: actions/checkout@v4.2.2 22 | - name: 🏗 Set up Poetry 23 | run: pipx install poetry 24 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 25 | id: python 26 | uses: actions/setup-python@v5.6.0 27 | with: 28 | python-version: ${{ env.DEFAULT_PYTHON }} 29 | cache: "poetry" 30 | - name: 🏗 Install workflow dependencies 31 | run: | 32 | poetry config virtualenvs.create true 33 | poetry config virtualenvs.in-project true 34 | - name: 🏗 Install dependencies 35 | run: poetry install --no-interaction 36 | - name: 🚀 Run mypy 37 | run: poetry run mypy PyViCare tests 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # IPython 77 | profile_default/ 78 | ipython_config.py 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # Environments 90 | .env 91 | .venv 92 | env/ 93 | venv/ 94 | ENV/ 95 | env.bak/ 96 | venv.bak/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | .dmypy.json 111 | dmypy.json 112 | 113 | # Pyre type checker 114 | .pyre/ 115 | token.bin 116 | PyViCare/__init__.py 117 | 118 | .DS_Store 119 | .vscode/settings.json 120 | 121 | *.save 122 | *.local.sh 123 | env* 124 | dump.*json 125 | 126 | # PyCharm 127 | .idea -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | build 3 | .pytest_cache 4 | .eggs 5 | dist 6 | .devcontainer 7 | .venv -------------------------------------------------------------------------------- /PyViCare/Feature.py: -------------------------------------------------------------------------------- 1 | # Feature flag to raise an exception in case of a non existing device feature. 2 | # The flag should be fully removed in a later release. 3 | # It allows dependent libraries to gracefully migrate to the new behaviour 4 | raise_exception_on_not_supported_device_feature = True 5 | 6 | # Feature flag to raise exception if rate limit of the API is hit 7 | raise_exception_on_rate_limit = True 8 | 9 | # Feature flag to raise exception on command calls if the API does not return (2xx or 3xx) responses. 10 | raise_exception_on_command_failure = True 11 | -------------------------------------------------------------------------------- /PyViCare/PyViCare.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime 3 | 4 | from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager 5 | from PyViCare.PyViCareBrowserOAuthManager import ViCareBrowserOAuthManager 6 | from PyViCare.PyViCareCachedService import ViCareCachedService 7 | from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig 8 | from PyViCare.PyViCareOAuthManager import ViCareOAuthManager 9 | from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService 10 | from PyViCare.PyViCareUtils import PyViCareInvalidDataError 11 | 12 | logger = logging.getLogger('ViCare') 13 | logger.addHandler(logging.NullHandler()) 14 | 15 | 16 | class PyViCare: 17 | """"Viessmann ViCare API Python tools""" 18 | def __init__(self) -> None: 19 | self.cacheDuration = 60 20 | 21 | def setCacheDuration(self, cache_duration): 22 | self.cacheDuration = int(cache_duration) 23 | 24 | def initWithCredentials(self, username: str, password: str, client_id: str, token_file: str): 25 | self.initWithExternalOAuth(ViCareOAuthManager( 26 | username, password, client_id, token_file)) 27 | 28 | def initWithExternalOAuth(self, oauth_manager: AbstractViCareOAuthManager) -> None: 29 | self.oauth_manager = oauth_manager 30 | self.__loadInstallations() 31 | 32 | def initWithBrowserOAuth(self, client_id: str, token_file: str) -> None: 33 | self.initWithExternalOAuth(ViCareBrowserOAuthManager(client_id, token_file)) 34 | 35 | def __buildService(self, accessor, roles): 36 | if self.cacheDuration > 0: 37 | return ViCareCachedService(self.oauth_manager, accessor, roles, self.cacheDuration) 38 | return ViCareService(self.oauth_manager, accessor, roles) 39 | 40 | def __loadInstallations(self): 41 | installations = self.oauth_manager.get( 42 | "/equipment/installations?includeGateways=true") 43 | if "data" not in installations: 44 | logger.error("Missing 'data' property when fetching installations") 45 | raise PyViCareInvalidDataError(installations) 46 | 47 | data = installations['data'] 48 | self.installations = Wrap(data) 49 | self.devices = list(self.__extract_devices()) 50 | 51 | def __extract_devices(self): 52 | for installation in self.installations: 53 | for gateway in installation.gateways: 54 | for device in gateway.devices: 55 | if device.deviceType not in ["heating", "zigbee", "vitoconnect", "electricityStorage", "tcu", "ventilation"]: 56 | continue # we are only interested in heating, photovoltaic, electricityStorage, and ventilation devices 57 | 58 | accessor = ViCareDeviceAccessor( 59 | installation.id, gateway.serial, device.id) 60 | service = self.__buildService(accessor, device.roles) 61 | 62 | logger.info("Device found: %s", device.modelId) 63 | 64 | yield PyViCareDeviceConfig(service, device.id, device.modelId, device.status) 65 | 66 | 67 | class DictWrap(object): 68 | def __init__(self, d): 69 | for k, v in d.items(): 70 | setattr(self, k, Wrap(v)) 71 | 72 | 73 | def Wrap(v): 74 | if isinstance(v, list): 75 | return [Wrap(x) for x in v] 76 | if isinstance(v, dict): 77 | return DictWrap(v) 78 | if isinstance(v, str) and len(v) == 24 and v[23] == 'Z' and v[10] == 'T': 79 | return datetime.strptime(v, '%Y-%m-%dT%H:%M:%S.%f%z') 80 | return v 81 | -------------------------------------------------------------------------------- /PyViCare/PyViCareAbstractOAuthManager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import abstractmethod 3 | from typing import Any 4 | 5 | from authlib.integrations.base_client import TokenExpiredError, InvalidTokenError 6 | from authlib.integrations.requests_client import OAuth2Session 7 | 8 | from PyViCare import Feature 9 | from PyViCare.PyViCareUtils import (PyViCareCommandError, 10 | PyViCareInternalServerError, 11 | PyViCareRateLimitError) 12 | 13 | logger = logging.getLogger('ViCare') 14 | logger.addHandler(logging.NullHandler()) 15 | 16 | API_BASE_URL = 'https://api.viessmann.com/iot/v2' 17 | 18 | 19 | class AbstractViCareOAuthManager: 20 | def __init__(self, oauth_session: OAuth2Session) -> None: 21 | self.__oauth = oauth_session 22 | 23 | @property 24 | def oauth_session(self) -> OAuth2Session: 25 | return self.__oauth 26 | 27 | def replace_session(self, new_session: OAuth2Session) -> None: 28 | self.__oauth = new_session 29 | 30 | @classmethod 31 | @abstractmethod 32 | def renewToken(self) -> None: 33 | return 34 | 35 | def get(self, url: str) -> Any: 36 | try: 37 | logger.debug(self.__oauth) 38 | response = self.__oauth.get(f"{API_BASE_URL}{url}", timeout=31).json() 39 | logger.debug("Response to get request: %s", response) 40 | self.__handle_expired_token(response) 41 | self.__handle_rate_limit(response) 42 | self.__handle_server_error(response) 43 | return response 44 | except TokenExpiredError: 45 | self.renewToken() 46 | return self.get(url) 47 | except InvalidTokenError: 48 | self.renewToken() 49 | return self.get(url) 50 | 51 | def __handle_expired_token(self, response): 52 | if ("error" in response and response["error"] == "EXPIRED TOKEN"): 53 | raise TokenExpiredError(response) 54 | 55 | def __handle_rate_limit(self, response): 56 | if not Feature.raise_exception_on_rate_limit: 57 | return 58 | 59 | if ("statusCode" in response and response["statusCode"] == 429): 60 | raise PyViCareRateLimitError(response) 61 | 62 | def __handle_server_error(self, response): 63 | if ("statusCode" in response and response["statusCode"] >= 500): 64 | raise PyViCareInternalServerError(response) 65 | 66 | def __handle_command_error(self, response): 67 | if not Feature.raise_exception_on_command_failure: 68 | return 69 | 70 | if ("statusCode" in response and response["statusCode"] >= 400): 71 | raise PyViCareCommandError(response) 72 | 73 | def post(self, url, data) -> Any: 74 | """POST URL using OAuth session. Automatically renew the token if needed 75 | Parameters 76 | ---------- 77 | url : str 78 | URL to get 79 | data : str 80 | Data to post 81 | 82 | Returns 83 | ------- 84 | result: json 85 | json representation of the answer 86 | """ 87 | headers = {"Content-Type": "application/json", 88 | "Accept": "application/vnd.siren+json"} 89 | try: 90 | response = self.__oauth.post( 91 | f"{API_BASE_URL}{url}", data, headers=headers).json() 92 | self.__handle_expired_token(response) 93 | self.__handle_rate_limit(response) 94 | self.__handle_command_error(response) 95 | return response 96 | except TokenExpiredError: 97 | self.renewToken() 98 | return self.post(url, data) 99 | except InvalidTokenError: 100 | self.renewToken() 101 | return self.post(url, data) 102 | -------------------------------------------------------------------------------- /PyViCare/PyViCareBrowserOAuthManager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import webbrowser 5 | from http.server import BaseHTTPRequestHandler, HTTPServer 6 | 7 | from authlib.common.security import generate_token 8 | from authlib.integrations.requests_client import OAuth2Session 9 | 10 | from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager 11 | from PyViCare.PyViCareUtils import (PyViCareBrowserOAuthTimeoutReachedError, 12 | PyViCareInvalidCredentialsError) 13 | 14 | logger = logging.getLogger('ViCare') 15 | logger.addHandler(logging.NullHandler()) 16 | 17 | AUTHORIZE_URL = 'https://iam.viessmann.com/idp/v3/authorize' 18 | TOKEN_URL = 'https://iam.viessmann.com/idp/v3/token' 19 | REDIRECT_PORT = 51125 20 | VIESSMANN_SCOPE = ["IoT User", "offline_access"] 21 | AUTH_TIMEOUT = 60 * 3 22 | 23 | 24 | class ViCareBrowserOAuthManager(AbstractViCareOAuthManager): 25 | class Serv(BaseHTTPRequestHandler): 26 | def __init__(self, callback, *args): 27 | self.callback = callback 28 | BaseHTTPRequestHandler.__init__(self, *args) 29 | 30 | def do_GET(self): 31 | (status_code, text) = self.callback(self.path) 32 | self.send_response(status_code) 33 | self.send_header("Content-type", "text/plain") 34 | self.end_headers() 35 | self.wfile.write(text.encode("utf-8")) 36 | 37 | def __init__(self, client_id: str, token_file: str) -> None: 38 | 39 | self.token_file = token_file 40 | self.client_id = client_id 41 | oauth_session = self.__load_or_create_new_session() 42 | super().__init__(oauth_session) 43 | 44 | def __load_or_create_new_session(self): 45 | restore_oauth = self.__restoreToken() 46 | if restore_oauth is not None: 47 | return restore_oauth 48 | return self.__execute_browser_authentication() 49 | 50 | def __execute_browser_authentication(self): 51 | redirect_uri = f"http://localhost:{REDIRECT_PORT}" 52 | oauth_session = OAuth2Session( 53 | self.client_id, redirect_uri=redirect_uri, scope=VIESSMANN_SCOPE, code_challenge_method='S256') 54 | code_verifier = generate_token(48) 55 | authorization_url, _ = oauth_session.create_authorization_url(AUTHORIZE_URL, code_verifier=code_verifier) 56 | 57 | webbrowser.open(authorization_url) 58 | 59 | location = None 60 | 61 | def callback(path): 62 | nonlocal location 63 | location = path 64 | return (200, "Success. You can close this browser window now.") 65 | 66 | def handlerWithCallbackWrapper(*args): 67 | ViCareBrowserOAuthManager.Serv(callback, *args) 68 | 69 | server = HTTPServer(('localhost', REDIRECT_PORT), 70 | handlerWithCallbackWrapper) 71 | server.timeout = AUTH_TIMEOUT 72 | server.handle_request() 73 | 74 | if location is None: 75 | logger.debug("Timeout reached") 76 | raise PyViCareBrowserOAuthTimeoutReachedError() 77 | 78 | logger.debug("Location: %s", location) 79 | 80 | oauth_session.fetch_token(TOKEN_URL, authorization_response=location, code_verifier=code_verifier) 81 | 82 | if oauth_session.token is None: 83 | raise PyViCareInvalidCredentialsError() 84 | 85 | logger.debug("Token received: %s", oauth_session.token) 86 | self.__storeToken(oauth_session.token) 87 | logger.info("New token created") 88 | return oauth_session 89 | 90 | def __storeToken(self, token): 91 | if self.token_file is None: 92 | return 93 | 94 | with open(self.token_file, mode='w') as json_file: 95 | json.dump(token, json_file) 96 | logger.info("Token stored to file") 97 | 98 | def __restoreToken(self): 99 | if self.token_file is None or not os.path.isfile(self.token_file): 100 | return None 101 | 102 | with open(self.token_file, mode='r') as json_file: 103 | token = json.load(json_file) 104 | logger.info("Token restored from file") 105 | return OAuth2Session(self.client_id, token=token) 106 | 107 | def renewToken(self) -> None: # type: ignore 108 | refresh_token = self.oauth_session.refresh_token 109 | self.oauth_session.refresh_token(TOKEN_URL, refresh_token=refresh_token) 110 | -------------------------------------------------------------------------------- /PyViCare/PyViCareCachedService.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import threading 3 | from typing import Any, List 4 | 5 | from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager 6 | from PyViCare.PyViCareService import (ViCareDeviceAccessor, ViCareService, 7 | readFeature) 8 | from PyViCare.PyViCareUtils import PyViCareInvalidDataError, ViCareTimer 9 | 10 | logger = logging.getLogger('ViCare') 11 | logger.addHandler(logging.NullHandler()) 12 | 13 | 14 | class ViCareCachedService(ViCareService): 15 | 16 | def __init__(self, oauth_manager: AbstractViCareOAuthManager, accessor: ViCareDeviceAccessor, roles: List[str], cacheDuration: int) -> None: 17 | ViCareService.__init__(self, oauth_manager, accessor, roles) 18 | self.__cacheDuration = cacheDuration 19 | self.__cache = None 20 | self.__cacheTime = None 21 | self.__lock = threading.Lock() 22 | 23 | def getProperty(self, property_name: str) -> Any: 24 | data = self.__get_or_update_cache() 25 | entities = data["data"] 26 | return readFeature(entities, property_name) 27 | 28 | def setProperty(self, property_name, action, data): 29 | response = super().setProperty(property_name, action, data) 30 | self.clear_cache() 31 | return response 32 | 33 | def __get_or_update_cache(self): 34 | with self.__lock: 35 | if self.is_cache_invalid(): 36 | # we always sett the cache time before we fetch the data 37 | # to avoid consuming all the api calls if the api is down 38 | # see https://github.com/home-assistant/core/issues/67052 39 | # we simply return the old cache in this case 40 | self.__cacheTime = ViCareTimer().now() 41 | 42 | data = self.fetch_all_features() 43 | if "data" not in data: 44 | logger.error("Missing 'data' property when fetching data.") 45 | raise PyViCareInvalidDataError(data) 46 | self.__cache = data 47 | return self.__cache 48 | 49 | def is_cache_invalid(self) -> bool: 50 | return self.__cache is None or self.__cacheTime is None or (ViCareTimer().now() - self.__cacheTime).seconds > self.__cacheDuration 51 | 52 | def clear_cache(self): 53 | with self.__lock: 54 | self.__cache = None 55 | self.__cacheTime = None 56 | -------------------------------------------------------------------------------- /PyViCare/PyViCareDevice.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from PyViCare.PyViCareService import ViCareService 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError, handleNotSupported 5 | 6 | 7 | class Device: 8 | """This is the base class for all simple devices. 9 | This class connects to the Viessmann ViCare API. 10 | The authentication is done through OAuth2. 11 | Note that currently, a new token is generated for each run. 12 | """ 13 | 14 | def __init__(self, service: ViCareService) -> None: 15 | self.service = service 16 | 17 | @handleNotSupported 18 | def getSerial(self): 19 | return self.service.getProperty("device.serial")["properties"]["value"]["value"] 20 | 21 | @handleNotSupported 22 | def getDeviceErrors(self) -> list[Any]: 23 | return list[Any](self.service.getProperty("device.messages.errors.raw")["properties"]["entries"]["value"]) 24 | 25 | def isLegacyDevice(self) -> bool: 26 | return self.service.hasRoles(["type:legacy"]) 27 | 28 | def isE3Device(self) -> bool: 29 | return self.service.hasRoles(["type:E3"]) 30 | 31 | def isDomesticHotWaterDevice(self): 32 | return self._isTypeDevice("heating.dhw") 33 | 34 | def isSolarThermalDevice(self): 35 | return self._isTypeDevice("heating.solar") 36 | 37 | def isVentilationDevice(self): 38 | return self._isTypeDevice("ventilation") 39 | 40 | def _isTypeDevice(self, deviceType: str): 41 | try: 42 | return self.service.getProperty(deviceType)["isEnabled"] and self.service.getProperty(deviceType)["properties"]["active"]["value"] 43 | except PyViCareNotSupportedFeatureError: 44 | return False 45 | -------------------------------------------------------------------------------- /PyViCare/PyViCareDeviceConfig.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import re 4 | 5 | from PyViCare.PyViCareFuelCell import FuelCell 6 | from PyViCare.PyViCareGazBoiler import GazBoiler 7 | from PyViCare.PyViCareHeatingDevice import HeatingDevice 8 | from PyViCare.PyViCareHeatPump import HeatPump 9 | from PyViCare.PyViCareHybrid import Hybrid 10 | from PyViCare.PyViCareOilBoiler import OilBoiler 11 | from PyViCare.PyViCarePelletsBoiler import PelletsBoiler 12 | from PyViCare.PyViCareRadiatorActuator import RadiatorActuator 13 | from PyViCare.PyViCareRoomSensor import RoomSensor 14 | from PyViCare.PyViCareElectricalEnergySystem import ElectricalEnergySystem 15 | from PyViCare.PyViCareGateway import Gateway 16 | from PyViCare.PyViCareVentilationDevice import VentilationDevice 17 | 18 | logger = logging.getLogger('ViCare') 19 | logger.addHandler(logging.NullHandler()) 20 | 21 | 22 | class PyViCareDeviceConfig: 23 | def __init__(self, service, device_id, device_model, status): 24 | self.service = service 25 | self.device_id = device_id 26 | self.device_model = device_model 27 | self.status = status 28 | 29 | def asGeneric(self): 30 | return HeatingDevice(self.service) 31 | 32 | def asGazBoiler(self): 33 | return GazBoiler(self.service) 34 | 35 | def asFuelCell(self): 36 | return FuelCell(self.service) 37 | 38 | def asHeatPump(self): 39 | return HeatPump(self.service) 40 | 41 | def asOilBoiler(self): 42 | return OilBoiler(self.service) 43 | 44 | def asPelletsBoiler(self): 45 | return PelletsBoiler(self.service) 46 | 47 | def asHybridDevice(self): 48 | return Hybrid(self.service) 49 | 50 | def asRadiatorActuator(self): 51 | return RadiatorActuator(self.service) 52 | 53 | def asRoomSensor(self): 54 | return RoomSensor(self.service) 55 | 56 | def asElectricalEnergySystem(self): 57 | return ElectricalEnergySystem(self.service) 58 | 59 | def asGateway(self): 60 | return Gateway(self.service) 61 | 62 | def asVentilation(self): 63 | return VentilationDevice(self.service) 64 | 65 | def getConfig(self): 66 | return self.service.accessor 67 | 68 | def getId(self): 69 | return self.device_id 70 | 71 | def getModel(self): 72 | return self.device_model 73 | 74 | def isOnline(self): 75 | return self.status == "Online" 76 | 77 | # see: https://vitodata300.viessmann.com/vd300/ApplicationHelp/VD300/1031_de_DE/Ger%C3%A4teliste.html 78 | def asAutoDetectDevice(self): 79 | device_types = [ 80 | (self.asFuelCell, r"Vitovalor|Vitocharge|Vitoblo", []), 81 | (self.asPelletsBoiler, r"Vitoligno|Ecotronic|VBC550P", []), 82 | (self.asOilBoiler, r"Vitoladens|Vitoradial|Vitorondens|VPlusH|V200KW2_6", []), 83 | (self.asGazBoiler, r"Vitodens|VScotH|Vitocrossal|VDensH|Vitopend|VPendH|OT_Heating_System", ["type:boiler"]), 84 | (self.asHeatPump, r"Vitocal|VBC70|V200WO1A|CU401B", ["type:heatpump"]), 85 | (self.asElectricalEnergySystem, r"E3_VitoCharge_03", ["type:ees"]), # ees, it this a typo? 86 | (self.asElectricalEnergySystem, r"E3_VitoCharge_05", ["type:ess"]), 87 | (self.asVentilation, r"E3_ViAir", ["type:ventilation"]), 88 | (self.asVentilation, r"E3_ViAir", ["type:ventilation;central"]), 89 | (self.asVentilation, r"E3_VitoPure", ["type:ventilation;purifier"]), 90 | (self.asRadiatorActuator, r"E3_RadiatorActuator", ["type:radiator"]), 91 | (self.asRoomSensor, r"E3_RoomSensor", ["type:climateSensor"]), 92 | (self.asGateway, r"E3_TCU41_x04", ["type:gateway;TCU100"]), 93 | (self.asGateway, r"E3_TCU19_x05", ["type:gateway;TCU200"]), 94 | (self.asGateway, r"E3_TCU10_x07", ["type:gateway;TCU300"]), 95 | (self.asGateway, r"Heatbox1", ["type:gateway;VitoconnectOpto1"]), 96 | (self.asGateway, r"Heatbox2", ["type:gateway;VitoconnectOpto2/OT2"]) 97 | ] 98 | 99 | for (creator_method, type_name, roles) in device_types: 100 | if re.search(type_name, self.device_model) or self.service.hasRoles(roles): 101 | logger.info("detected %s %s", self.device_model, creator_method.__name__) 102 | return creator_method() 103 | 104 | logger.info("Could not auto detect %s. Use generic device.", self.device_model) 105 | return self.asGeneric() 106 | 107 | def get_raw_json(self): 108 | return self.service.fetch_all_features() 109 | 110 | def dump_secure(self, flat=False): 111 | if flat: 112 | inner = ',\n'.join([json.dumps(x, sort_keys=True) for x in self.get_raw_json()['data']]) 113 | outer = json.dumps({'data': ['placeholder']}, indent=0) 114 | dumpJSON = outer.replace('"placeholder"', inner) 115 | else: 116 | dumpJSON = json.dumps(self.get_raw_json(), indent=4, sort_keys=True) 117 | 118 | def repl(m): 119 | return m.group(1) + ('#' * len(m.group(2))) + m.group(3) 120 | 121 | return re.sub(r'(["\/])(\d{6,})(["\/])', repl, dumpJSON) 122 | -------------------------------------------------------------------------------- /PyViCare/PyViCareElectricalEnergySystem.py: -------------------------------------------------------------------------------- 1 | from PyViCare.PyViCareDevice import Device 2 | from PyViCare.PyViCareUtils import handleNotSupported 3 | 4 | 5 | class ElectricalEnergySystem(Device): 6 | 7 | @handleNotSupported 8 | def getPointOfCommonCouplingTransferPowerExchange(self): 9 | return self.service.getProperty("pcc.transfer.power.exchange")["properties"][ 10 | "value" 11 | ]["value"] 12 | 13 | @handleNotSupported 14 | def getPhotovoltaicProductionCumulatedUnit(self): 15 | return self.service.getProperty("photovoltaic.production.cumulated")[ 16 | "properties" 17 | ]["currentDay"]["unit"] 18 | 19 | @handleNotSupported 20 | def getPhotovoltaicProductionCumulatedCurrentDay(self): 21 | return self.service.getProperty("photovoltaic.production.cumulated")[ 22 | "properties" 23 | ]["currentDay"]["value"] 24 | 25 | @handleNotSupported 26 | def getPhotovoltaicProductionCumulatedCurrentWeek(self): 27 | return self.service.getProperty("photovoltaic.production.cumulated")[ 28 | "properties" 29 | ]["currentWeek"]["value"] 30 | 31 | @handleNotSupported 32 | def getPhotovoltaicProductionCumulatedCurrentMonth(self): 33 | return self.service.getProperty("photovoltaic.production.cumulated")[ 34 | "properties" 35 | ]["currentMonth"]["value"] 36 | 37 | @handleNotSupported 38 | def getPhotovoltaicProductionCumulatedCurrentYear(self): 39 | return self.service.getProperty("photovoltaic.production.cumulated")[ 40 | "properties" 41 | ]["currentYear"]["value"] 42 | 43 | @handleNotSupported 44 | def getPhotovoltaicProductionCumulatedLifeCycle(self): 45 | return self.service.getProperty("photovoltaic.production.cumulated")[ 46 | "properties" 47 | ]["lifeCycle"]["value"] 48 | 49 | @handleNotSupported 50 | def getPhotovoltaicStatus(self): 51 | return self.service.getProperty("photovoltaic.status")["properties"]["status"][ 52 | "value" 53 | ] 54 | 55 | @handleNotSupported 56 | def getPhotovoltaicProductionCurrent(self): 57 | return self.service.getProperty("photovoltaic.production.current")[ 58 | "properties" 59 | ]["value"]["value"] 60 | 61 | @handleNotSupported 62 | def getPhotovoltaicProductionCurrentUnit(self): 63 | return self.service.getProperty("photovoltaic.production.current")[ 64 | "properties" 65 | ]["value"]["unit"] 66 | 67 | @handleNotSupported 68 | def getPointOfCommonCouplingTransferConsumptionTotal(self): 69 | return self.service.getProperty("pcc.transfer.consumption.total")["properties"][ 70 | "value" 71 | ]["value"] 72 | 73 | @handleNotSupported 74 | def getPointOfCommonCouplingTransferConsumptionTotalUnit(self): 75 | return self.service.getProperty("pcc.transfer.consumption.total")["properties"][ 76 | "value" 77 | ]["unit"] 78 | 79 | @handleNotSupported 80 | def getPointOfCommonCouplingTransferFeedInTotal(self): 81 | return self.service.getProperty("pcc.transfer.feedIn.total")["properties"][ 82 | "value" 83 | ]["value"] 84 | 85 | @handleNotSupported 86 | def getPointOfCommonCouplingTransferFeedInTotalUnit(self): 87 | return self.service.getProperty("pcc.transfer.feedIn.total")["properties"][ 88 | "value" 89 | ]["unit"] 90 | 91 | @handleNotSupported 92 | def getElectricalEnergySystemTransferDischargeCumulatedUnit(self): 93 | return self.service.getProperty("ess.transfer.discharge.cumulated")[ 94 | "properties" 95 | ]["currentDay"]["unit"] 96 | 97 | @handleNotSupported 98 | def getElectricalEnergySystemTransferDischargeCumulatedCurrentDay(self): 99 | return self.service.getProperty("ess.transfer.discharge.cumulated")[ 100 | "properties" 101 | ]["currentDay"]["value"] 102 | 103 | @handleNotSupported 104 | def getElectricalEnergySystemTransferDischargeCumulatedCurrentWeek(self): 105 | return self.service.getProperty("ess.transfer.discharge.cumulated")[ 106 | "properties" 107 | ]["currentWeek"]["value"] 108 | 109 | @handleNotSupported 110 | def getElectricalEnergySystemTransferDischargeCumulatedCurrentMonth(self): 111 | return self.service.getProperty("ess.transfer.discharge.cumulated")[ 112 | "properties" 113 | ]["currentMonth"]["value"] 114 | 115 | @handleNotSupported 116 | def getElectricalEnergySystemTransferDischargeCumulatedCurrentYear(self): 117 | return self.service.getProperty("ess.transfer.discharge.cumulated")[ 118 | "properties" 119 | ]["currentYear"]["value"] 120 | 121 | @handleNotSupported 122 | def getElectricalEnergySystemTransferDischargeCumulatedLifeCycle(self): 123 | return self.service.getProperty("ess.transfer.discharge.cumulated")[ 124 | "properties" 125 | ]["lifeCycle"]["value"] 126 | 127 | @handleNotSupported 128 | def getElectricalEnergySystemSOC(self): 129 | return self.service.getProperty("ess.stateOfCharge")["properties"]["value"][ 130 | "value" 131 | ] 132 | 133 | @handleNotSupported 134 | def getElectricalEnergySystemSOCUnit(self): 135 | return self.service.getProperty("ess.stateOfCharge")["properties"]["value"][ 136 | "unit" 137 | ] 138 | 139 | @handleNotSupported 140 | def getElectricalEnergySystemPower(self): 141 | return self.service.getProperty("ess.power")["properties"]["value"]["value"] 142 | 143 | @handleNotSupported 144 | def getElectricalEnergySystemPowerUnit(self): 145 | return self.service.getProperty("ess.power")["properties"]["value"]["unit"] 146 | 147 | @handleNotSupported 148 | def getElectricalEnergySystemOperationState(self): 149 | return self.service.getProperty("ess.operationState")["properties"]["value"][ 150 | "value" 151 | ] 152 | -------------------------------------------------------------------------------- /PyViCare/PyViCareGateway.py: -------------------------------------------------------------------------------- 1 | from PyViCare.PyViCareDevice import Device 2 | from PyViCare.PyViCareUtils import handleNotSupported 3 | 4 | 5 | class Gateway(Device): 6 | 7 | @handleNotSupported 8 | def getSerial(self): 9 | return self.service.getProperty("gateway.devices")["gatewayId"] 10 | 11 | @handleNotSupported 12 | def getWifiSignalStrength(self): 13 | return self.service.getProperty("gateway.wifi")["properties"]["strength"]["value"] 14 | -------------------------------------------------------------------------------- /PyViCare/PyViCareHeatCurveCalculation.py: -------------------------------------------------------------------------------- 1 | # based on feedback in: https://github.com/somm15/PyViCare/issues/238 2 | 3 | # gas burner and if device has roles "type:heatpump", "type:E3" 4 | def heat_curve_formular_variant1(delta, inside, shift, slope): 5 | target_supply = (inside + shift - slope * delta 6 | * (1.4347 + 0.021 * delta + 247.9 7 | * pow(10, -6) * pow(delta, 2))) 8 | return target_supply 9 | 10 | 11 | # heatpump has roles "type:heatpump" and with single circuit 12 | def heat_curve_formular_variant2(delta, inside, shift, slope): 13 | target_supply = (inside + shift - slope * delta 14 | * (1.148987 + 0.021 * delta + 247.9 15 | * pow(10, -6) * pow(delta, 2)) + 5) 16 | return target_supply 17 | -------------------------------------------------------------------------------- /PyViCare/PyViCareHybrid.py: -------------------------------------------------------------------------------- 1 | from PyViCare.PyViCareGazBoiler import GazBoiler 2 | from PyViCare.PyViCareHeatPump import HeatPump 3 | 4 | 5 | class Hybrid(GazBoiler, HeatPump): 6 | pass 7 | -------------------------------------------------------------------------------- /PyViCare/PyViCareOAuthManager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import pickle 4 | from contextlib import suppress 5 | from pickle import UnpicklingError 6 | 7 | import requests 8 | from authlib.common.security import generate_token 9 | from authlib.integrations.requests_client import OAuth2Session 10 | 11 | from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager 12 | from PyViCare.PyViCareUtils import (PyViCareInvalidConfigurationError, 13 | PyViCareInvalidCredentialsError) 14 | 15 | logger = logging.getLogger('ViCare') 16 | logger.addHandler(logging.NullHandler()) 17 | 18 | AUTHORIZE_URL = 'https://iam.viessmann.com/idp/v3/authorize' 19 | TOKEN_URL = 'https://iam.viessmann.com/idp/v3/token' 20 | REDIRECT_URI = "vicare://oauth-callback/everest" 21 | VIESSMANN_SCOPE = ["IoT User"] 22 | 23 | 24 | class ViCareOAuthManager(AbstractViCareOAuthManager): 25 | def __init__(self, username, password, client_id, token_file): 26 | self.username = username 27 | self.password = password 28 | self.token_file = token_file 29 | self.client_id = client_id 30 | oauth_session = self.__restore_oauth_session_from_token(token_file) 31 | super().__init__(oauth_session) 32 | 33 | def __restore_oauth_session_from_token(self, token_file): 34 | existing_token = self.__deserialize_token(token_file) 35 | if existing_token is not None: 36 | return OAuth2Session(self.client_id, token=existing_token) 37 | 38 | return self.__create_new_session(self.username, self.password, token_file) 39 | 40 | def __create_new_session(self, username, password, token_file=None): 41 | """Create a new oAuth2 sessions 42 | Viessmann tokens expire after 3600s (60min) 43 | Parameters 44 | ---------- 45 | username : str 46 | e-mail address 47 | password : str 48 | password 49 | token_file: str 50 | path to serialize the token (will restore if already existing). No serialisation if not present 51 | 52 | Returns 53 | ------- 54 | oauth: 55 | oauth sessions object 56 | """ 57 | oauth_session = OAuth2Session( 58 | self.client_id, redirect_uri=REDIRECT_URI, scope=VIESSMANN_SCOPE, code_challenge_method='S256') 59 | code_verifier = generate_token(48) 60 | authorization_url, _ = oauth_session.create_authorization_url(AUTHORIZE_URL, code_verifier=code_verifier) 61 | logger.debug("Auth URL is: %s", authorization_url) 62 | 63 | header = {'Content-Type': 'application/x-www-form-urlencoded'} 64 | response = requests.post( 65 | authorization_url, headers=header, auth=(username, password), allow_redirects=False) 66 | 67 | if response.status_code == 401: 68 | raise PyViCareInvalidConfigurationError(response.json()) 69 | 70 | if 'Location' not in response.headers: 71 | logger.debug('Response: %s', response) 72 | raise PyViCareInvalidCredentialsError() 73 | 74 | oauth_session.fetch_token(TOKEN_URL, authorization_response=response.headers['Location'], code_verifier=code_verifier) 75 | 76 | if oauth_session.token is None: 77 | raise PyViCareInvalidCredentialsError() 78 | 79 | logger.debug("Token received: %s",oauth_session.token) 80 | self.__serialize_token(oauth_session.token, token_file) 81 | logger.info("New token created") 82 | return oauth_session 83 | 84 | def renewToken(self): 85 | logger.info("Token expired, renewing") 86 | self.replace_session(self.__create_new_session( 87 | self.username, self.password, self.token_file)) 88 | logger.info("Token renewed successfully") 89 | 90 | def __serialize_token(self, oauth, token_file): 91 | logger.debug("Start serial") 92 | if token_file is None: 93 | logger.debug("Skip serial, no file provided.") 94 | return 95 | 96 | with open(token_file, mode='wb') as binary_file: 97 | pickle.dump(oauth, binary_file) 98 | 99 | logger.info("Token serialized to %s", token_file) 100 | 101 | def __deserialize_token(self, token_file): 102 | if token_file is None or not os.path.isfile(token_file): 103 | logger.debug( 104 | "Token file argument not provided or file does not exist") 105 | return None 106 | 107 | logger.info("Token file exists") 108 | with suppress(UnpicklingError): 109 | with open(token_file, mode='rb') as binary_file: 110 | s_token = pickle.load(binary_file) 111 | logger.info("Token restored from file") 112 | return s_token 113 | logger.warning("Could not restore token") 114 | return None 115 | -------------------------------------------------------------------------------- /PyViCare/PyViCareOilBoiler.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from PyViCare.PyViCareHeatingDevice import (HeatingDevice, 4 | HeatingDeviceWithComponent, 5 | get_available_burners) 6 | from PyViCare.PyViCareUtils import handleNotSupported 7 | 8 | 9 | class OilBoiler(HeatingDevice): 10 | 11 | @property 12 | def burners(self) -> List[Any]: 13 | return list([self.getBurner(x) for x in self.getAvailableBurners()]) 14 | 15 | def getBurner(self, burner): 16 | return OilBurner(self, burner) 17 | 18 | @handleNotSupported 19 | def getAvailableBurners(self): 20 | return get_available_burners(self.service) 21 | 22 | @handleNotSupported 23 | def getBoilerTemperature(self): 24 | return self.service.getProperty("heating.boiler.sensors.temperature.main")["properties"]["value"]["value"] 25 | 26 | 27 | class OilBurner(HeatingDeviceWithComponent): 28 | 29 | @property 30 | def burner(self) -> str: 31 | return self.component 32 | 33 | @handleNotSupported 34 | def getActive(self): 35 | return self.service.getProperty(f"heating.burners.{self.burner}")["properties"]["active"]["value"] 36 | 37 | @handleNotSupported 38 | def getHours(self): 39 | return self.service.getProperty(f"heating.burners.{self.burner}.statistics")["properties"]["hours"]["value"] 40 | 41 | @handleNotSupported 42 | def getStarts(self): 43 | return self.service.getProperty(f"heating.burners.{self.burner}.statistics")["properties"]["starts"]["value"] 44 | 45 | @handleNotSupported 46 | def getModulation(self): 47 | return self.service.getProperty(f"heating.burners.{self.burner}.modulation")["properties"]["value"]["value"] 48 | -------------------------------------------------------------------------------- /PyViCare/PyViCarePelletsBoiler.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import List 3 | 4 | from PyViCare.PyViCareHeatingDevice import (HeatingDevice, HeatingDeviceWithComponent, get_available_burners) 5 | from PyViCare.PyViCareUtils import handleNotSupported 6 | 7 | 8 | class PelletsBoiler(HeatingDevice): 9 | 10 | @property 11 | def burners(self) -> List[PelletsBurner]: 12 | return list([self.getBurner(x) for x in self.getAvailableBurners()]) 13 | 14 | def getBurner(self, burner) -> PelletsBurner: 15 | return PelletsBurner(self, burner) 16 | 17 | @handleNotSupported 18 | def getAvailableBurners(self): 19 | return get_available_burners(self.service) 20 | 21 | @handleNotSupported 22 | def getBoilerTemperature(self): 23 | return self.service.getProperty("heating.boiler.sensors.temperature.main")["properties"]["value"]["value"] 24 | 25 | @handleNotSupported 26 | def getAshLevel(self): 27 | return self.service.getProperty('heating.boiler.ash.level.current')['properties']['value']['value'] 28 | 29 | @handleNotSupported 30 | def getAirFlapsPrimaryPosition(self): 31 | return self.service.getProperty('heating.boiler.airflaps.0.position.current')['properties']['value']['value'] 32 | 33 | @handleNotSupported 34 | def getAirFlapsSecondaryPosition(self): 35 | return self.service.getProperty('heating.boiler.airflaps.1.position.current')['properties']['value']['value'] 36 | 37 | @handleNotSupported 38 | def getExhaustO2Level(self): 39 | return self.service.getProperty('heating.flue.sensors.o2.lambda')['properties']['value']['value'] 40 | 41 | @handleNotSupported 42 | def getBoilerCuircuitPumpCurrentLevel(self): 43 | return self.service.getProperty('heating.boiler.pumps.circuit.power.current')['properties']['value']['value'] 44 | 45 | @handleNotSupported 46 | def getBoilerReturnTemperature(self): 47 | return self.service.getProperty('heating.sensors.temperature.return')['properties']['value']['value'] 48 | 49 | @handleNotSupported 50 | def getFlueTemperature(self): 51 | return self.service.getProperty('heating.flue.sensors.temperature.main')['properties']['value']['value'] 52 | 53 | @handleNotSupported 54 | def getFuelNeed(self): 55 | return self.service.getProperty('heating.configuration.fuel.need')['properties']['value']['value'] 56 | 57 | @handleNotSupported 58 | def getFuelUnit(self) -> str: 59 | return str(self.service.getProperty('heating.configuration.fuel.need')['properties']['value']['unit']) 60 | 61 | @handleNotSupported 62 | def getBoilerState(self): 63 | return self.service.getProperty('heating.boiler.operating.phase')['properties']['value']['value'] 64 | 65 | @handleNotSupported 66 | def getBoilerCuircuitPumpStatus(self): 67 | return self.service.getProperty('heating.boiler.pumps.circuit')['properties']['status']['value'] 68 | @handleNotSupported 69 | def getBufferMainTemperature(self): 70 | return self.service.getProperty("heating.bufferCylinder.sensors.temperature.main")["properties"]['value']['value'] 71 | 72 | @handleNotSupported 73 | def getBufferTopTemperature(self): 74 | return self.service.getProperty("heating.bufferCylinder.sensors.temperature.top")["properties"]['value']['value'] 75 | 76 | @handleNotSupported 77 | def getBufferMidTopTemperature(self): 78 | return self.service.getProperty("heating.bufferCylinder.sensors.temperature.midTop")["properties"]['value']['value'] 79 | 80 | @handleNotSupported 81 | def getBufferMiddleTemperature(self): 82 | return self.service.getProperty("heating.bufferCylinder.sensors.temperature.middle")["properties"]['value']['value'] 83 | 84 | @handleNotSupported 85 | def getBufferMidBottomTemperature(self): 86 | return self.service.getProperty("heating.bufferCylinder.sensors.temperature.midBottom")["properties"]['value']['value'] 87 | 88 | @handleNotSupported 89 | def getBufferBottomTemperature(self): 90 | return self.service.getProperty("heating.bufferCylinder.sensors.temperature.bottom")["properties"]['value']['value'] 91 | 92 | class PelletsBurner(HeatingDeviceWithComponent): 93 | 94 | @property 95 | def burner(self) -> str: 96 | return self.component 97 | 98 | @handleNotSupported 99 | def getActive(self) -> bool: 100 | return bool(self.service.getProperty(f"heating.burners.{self.burner}")["properties"]["active"]["value"]) 101 | 102 | @handleNotSupported 103 | def getHours(self) -> float: 104 | return float(self.service.getProperty(f"heating.burners.{self.burner}.statistics")["properties"]["hours"]["value"]) 105 | 106 | @handleNotSupported 107 | def getStarts(self) -> int: 108 | return int(self.service.getProperty(f"heating.burners.{self.burner}.statistics")["properties"]["starts"]["value"]) 109 | -------------------------------------------------------------------------------- /PyViCare/PyViCareRadiatorActuator.py: -------------------------------------------------------------------------------- 1 | from PyViCare.PyViCareDevice import Device 2 | from PyViCare.PyViCareUtils import handleAPICommandErrors, handleNotSupported 3 | 4 | 5 | class RadiatorActuator(Device): 6 | 7 | @handleNotSupported 8 | def getSerial(self): 9 | return self.service.getProperty("device.name")["deviceId"] 10 | 11 | @handleNotSupported 12 | def getBatteryLevel(self) -> int: 13 | return int(self.service.getProperty("device.power.battery")["properties"]["level"]["value"]) 14 | 15 | @handleNotSupported 16 | def getTemperature(self): 17 | return self.service.getProperty("device.sensors.temperature")["properties"]["value"]["value"] 18 | 19 | @handleNotSupported 20 | def getTargetTemperature(self): 21 | return self.service.getProperty("trv.temperature")["properties"]["value"]["value"] 22 | 23 | @handleAPICommandErrors 24 | def setTargetTemperature(self, temperature): 25 | return self.service.setProperty("trv.temperature", "setTargetTemperature", {'temperature': float(temperature)}) 26 | -------------------------------------------------------------------------------- /PyViCare/PyViCareRoomSensor.py: -------------------------------------------------------------------------------- 1 | from PyViCare.PyViCareDevice import Device 2 | from PyViCare.PyViCareUtils import handleNotSupported 3 | 4 | 5 | class RoomSensor(Device): 6 | 7 | @handleNotSupported 8 | def getSerial(self): 9 | return self.service.getProperty("device.sensors.temperature")["deviceId"] 10 | 11 | @handleNotSupported 12 | def getBatteryLevel(self) -> int: 13 | return int(self.service.getProperty("device.power.battery")["properties"]["level"]["value"]) 14 | 15 | @handleNotSupported 16 | def getTemperature(self): 17 | return self.service.getProperty("device.sensors.temperature")["properties"]["value"]["value"] 18 | 19 | @handleNotSupported 20 | def getHumidity(self): 21 | return self.service.getProperty("device.sensors.humidity")["properties"]["value"]["value"] 22 | -------------------------------------------------------------------------------- /PyViCare/PyViCareService.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Any, List 4 | 5 | from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager 6 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 7 | 8 | logger = logging.getLogger('ViCare') 9 | logger.addHandler(logging.NullHandler()) 10 | 11 | def readFeature(entities, property_name): 12 | feature = next( 13 | (f for f in entities if f["feature"] == property_name), None) 14 | 15 | if feature is None: 16 | raise PyViCareNotSupportedFeatureError(property_name) 17 | 18 | return feature 19 | 20 | def hasRoles(requested_roles: List[str], existing_roles: List[str]) -> bool: 21 | return len(requested_roles) > 0 and set(requested_roles).issubset(set(existing_roles)) 22 | 23 | def buildSetPropertyUrl(accessor, property_name, action): 24 | return f'/features/installations/{accessor.id}/gateways/{accessor.serial}/devices/{accessor.device_id}/features/{property_name}/commands/{action}' 25 | 26 | class ViCareDeviceAccessor: 27 | def __init__(self, _id: int, serial: str, device_id: str) -> None: 28 | self.id = _id 29 | self.serial = serial 30 | self.device_id = device_id 31 | 32 | class ViCareService: 33 | def __init__(self, oauth_manager: AbstractViCareOAuthManager, accessor: ViCareDeviceAccessor, roles: List[str]) -> None: 34 | self.oauth_manager = oauth_manager 35 | self.accessor = accessor 36 | self.roles = roles 37 | 38 | def getProperty(self, property_name: str) -> Any: 39 | url = self.buildGetPropertyUrl(property_name) 40 | return self.oauth_manager.get(url) 41 | 42 | def buildGetPropertyUrl(self, property_name): 43 | if self._isGateway(): 44 | return f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/features/{property_name}' 45 | return f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/devices/{self.accessor.device_id}/features/{property_name}' 46 | 47 | def hasRoles(self, requested_roles) -> bool: 48 | return hasRoles(requested_roles, self.roles) 49 | 50 | def _isGateway(self) -> bool: 51 | return self.hasRoles(["type:gateway;VitoconnectOpto1"]) or self.hasRoles(["type:gateway;VitoconnectOpto2/OT2"]) or self.hasRoles(["type:gateway;TCU100"]) or self.hasRoles(["type:gateway;TCU200"]) or self.hasRoles(["type:gateway;TCU300"]) 52 | 53 | def setProperty(self, property_name: str, action: str, data: Any) -> Any: 54 | url = buildSetPropertyUrl( 55 | self.accessor, property_name, action) 56 | 57 | post_data = data if isinstance(data, str) else json.dumps(data) 58 | return self.oauth_manager.post(url, post_data) 59 | 60 | def fetch_all_features(self) -> Any: 61 | url = f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/devices/{self.accessor.device_id}/features/' 62 | if self._isGateway(): 63 | url = f'/features/installations/{self.accessor.id}/gateways/{self.accessor.serial}/features/' 64 | return self.oauth_manager.get(url) 65 | -------------------------------------------------------------------------------- /PyViCare/PyViCareUtils.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from functools import wraps 3 | from typing import Callable 4 | 5 | from PyViCare import Feature 6 | 7 | VICARE_DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] 8 | 9 | # This decorator handles access to underlying JSON properties. 10 | # If the property is not found (KeyError) or the index does not 11 | # exists (IndexError), the requested feature is not supported by 12 | # the device. 13 | 14 | 15 | def isSupported(method: Callable) -> bool: 16 | try: 17 | result = method() 18 | return bool(result != 'error') 19 | except PyViCareNotSupportedFeatureError: 20 | return False 21 | 22 | 23 | def time_as_delta(date_time: datetime) -> timedelta: 24 | return timedelta( 25 | hours=date_time.hour, 26 | minutes=date_time.minute, 27 | seconds=date_time.second 28 | ) 29 | 30 | 31 | def parse_time_as_delta(time_string: str) -> timedelta: 32 | return timedelta( 33 | hours=int(time_string[0:2]), 34 | minutes=int(time_string[3:5]) 35 | ) 36 | 37 | 38 | class ViCareTimer: 39 | # class is used to replace logic in unittest 40 | def now(self) -> datetime: 41 | return datetime.now() 42 | 43 | 44 | def handleNotSupported(func: Callable) -> Callable: 45 | @wraps(func) 46 | def wrapper(*args, **kwargs): 47 | try: 48 | return func(*args, **kwargs) 49 | except (KeyError, IndexError): 50 | raise PyViCareNotSupportedFeatureError(func.__name__) 51 | 52 | # You can remove that wrapper after the feature flag gets removed entirely. 53 | def feature_flag_wrapper(*args, **kwargs): 54 | try: 55 | return wrapper(*args, **kwargs) 56 | except PyViCareNotSupportedFeatureError: 57 | if Feature.raise_exception_on_not_supported_device_feature: 58 | raise 59 | return "error" 60 | return feature_flag_wrapper 61 | 62 | 63 | def handleAPICommandErrors(func: Callable) -> Callable: 64 | @wraps(func) 65 | def wrapper(*args, **kwargs): 66 | try: 67 | return func(*args, **kwargs) 68 | except (KeyError, IndexError): 69 | raise PyViCareCommandError(func.__name__) 70 | 71 | # You can remove that wrapper after the feature flag gets removed entirely. 72 | def feature_flag_wrapper(*args, **kwargs): 73 | try: 74 | return wrapper(*args, **kwargs) 75 | except PyViCareCommandError: 76 | if Feature.raise_exception_on_command_failure: 77 | raise 78 | return "error" 79 | return feature_flag_wrapper 80 | 81 | 82 | class PyViCareNotSupportedFeatureError(Exception): 83 | pass 84 | 85 | 86 | class PyViCareInvalidConfigurationError(Exception): 87 | def __init__(self, response): 88 | error = response['error'] 89 | error_description = response['error_description'] 90 | 91 | msg = f'Invalid credentials. Error: {error}. Description: {error_description}. Please check your configuration: clientid and redirect uri.' 92 | super().__init__(self, msg) 93 | self.message = msg 94 | 95 | 96 | class PyViCareInvalidCredentialsError(Exception): 97 | pass 98 | 99 | 100 | class PyViCareBrowserOAuthTimeoutReachedError(Exception): 101 | pass 102 | 103 | 104 | class PyViCareInvalidDataError(Exception): 105 | pass 106 | 107 | 108 | class PyViCareRateLimitError(Exception): 109 | 110 | def __init__(self, response): 111 | extended_payload = response["extendedPayload"] 112 | name = extended_payload["name"] 113 | requestCountLimit = extended_payload["requestCountLimit"] 114 | limitReset = extended_payload["limitReset"] 115 | limitResetDate = datetime.utcfromtimestamp(limitReset / 1000) 116 | 117 | msg = f'API rate limit {name} exceeded. Max {requestCountLimit} calls in timewindow. Limit reset at {limitResetDate.isoformat()}.' 118 | 119 | super().__init__(self, msg) 120 | self.message = msg 121 | self.limitResetDate = limitResetDate 122 | 123 | 124 | class PyViCareInternalServerError(Exception): 125 | def __init__(self, response): 126 | statusCode = response["statusCode"] 127 | 128 | message = response["message"] 129 | viErrorId = response["viErrorId"] 130 | 131 | msg = f'Request failed with status code {statusCode} and message "{message}". ViCare ErrorId: {viErrorId}' 132 | 133 | super().__init__(self, msg) 134 | self.message = msg 135 | 136 | 137 | class PyViCareCommandError(Exception): 138 | def __init__(self, response): 139 | if isinstance(response, str): 140 | msg = f'Command failed with message "{response}"' 141 | else: 142 | statusCode = response["statusCode"] 143 | extended_payload = response["extendedPayload"] 144 | 145 | if "reason" in extended_payload: 146 | reason = extended_payload["reason"] 147 | else: 148 | reason = "Unknown" 149 | 150 | msg = f'Command failed with status code {statusCode}. Reason given was: {reason}' 151 | 152 | super().__init__(self, msg) 153 | self.message = msg 154 | -------------------------------------------------------------------------------- /PyViCare/PyViCareVentilationDevice.py: -------------------------------------------------------------------------------- 1 | from contextlib import suppress 2 | from deprecated import deprecated 3 | 4 | from PyViCare.PyViCareDevice import Device 5 | from PyViCare.PyViCareUtils import (PyViCareNotSupportedFeatureError, handleAPICommandErrors, handleNotSupported) 6 | 7 | 8 | class VentilationDevice(Device): 9 | """This is the base class for all ventilation devices. 10 | This class connects to the Viessmann ViCare API. 11 | The authentication is done through OAuth2. 12 | Note that currently, a new token is generated for each run. 13 | """ 14 | 15 | @handleNotSupported 16 | def getVentilationDemand(self) -> str: 17 | return str(self.service.getProperty("ventilation.operating.state")["properties"]["demand"]["value"]) 18 | 19 | @handleNotSupported 20 | def getVentilationReason(self) -> str: 21 | return str(self.service.getProperty("ventilation.operating.state")["properties"]["reason"]["value"]) 22 | 23 | @handleNotSupported 24 | def getVentilationModes(self) -> list[str]: 25 | return list[str](self.service.getProperty("ventilation.operating.modes.active")["commands"]["setMode"]["params"]["mode"]["constraints"]["enum"]) 26 | 27 | @handleNotSupported 28 | @deprecated(reason="renamed, use getVentilationModes", version="2.40.0") 29 | def getAvailableModes(self): 30 | return self.getVentilationModes() 31 | 32 | @handleNotSupported 33 | def getVentilationMode(self, mode: str) -> bool: 34 | return bool(self.service.getProperty(f"ventilation.operating.modes.{mode}")["properties"]["active"]["value"]) 35 | 36 | @handleNotSupported 37 | def getActiveVentilationMode(self) -> str: 38 | return str(self.service.getProperty("ventilation.operating.modes.active")["properties"]["value"]["value"]) 39 | 40 | @handleNotSupported 41 | def getVentilationLevels(self) -> list[str]: 42 | return list[str](self.service.getProperty("ventilation.operating.modes.permanent")["commands"]["setLevel"]["params"]["level"]["constraints"]["enum"]) 43 | 44 | @handleNotSupported 45 | @deprecated(reason="renamed, use getVentilationLevels", version="2.40.0") 46 | def getPermanentLevels(self) -> list[str]: 47 | return list[str](self.getVentilationLevels()) 48 | 49 | @handleNotSupported 50 | def getVentilationLevel(self) -> str: 51 | return str(self.service.getProperty("ventilation.operating.state")["properties"]["level"]["value"]) 52 | 53 | @handleAPICommandErrors 54 | def setVentilationLevel(self, level: str): 55 | return self.service.setProperty("ventilation.operating.modes.permanent", "setLevel", {'level': level}) 56 | 57 | @handleAPICommandErrors 58 | @deprecated(reason="renamed, use setVentilationLevel", version="2.40.0") 59 | def setPermanentLevel(self, level: str): 60 | return self.setVentilationLevel(level) 61 | 62 | @handleNotSupported 63 | @deprecated(reason="renamed, use getActiveVentilationMode", version="2.40.0") 64 | def getActiveMode(self): 65 | return self.getActiveVentilationMode() 66 | 67 | def activateVentilationMode(self, mode: str): 68 | """ Set the active ventilation mode 69 | Parameters 70 | ---------- 71 | mode : str 72 | Valid mode can be obtained using getVentilationModes() 73 | 74 | Returns 75 | ------- 76 | result: json 77 | json representation of the answer 78 | """ 79 | return self.service.setProperty("ventilation.operating.modes.active", "setMode", {'mode': mode}) 80 | 81 | @deprecated(reason="renamed, use activateVentilationMode", version="2.40.0") 82 | def setActiveMode(self, mode): 83 | """ Set the active mode 84 | Parameters 85 | ---------- 86 | mode : str 87 | Valid mode can be obtained using getModes() 88 | 89 | Returns 90 | ------- 91 | result: json 92 | json representation of the answer 93 | """ 94 | return self.activateVentilationMode(mode) 95 | 96 | @handleNotSupported 97 | def getVentilationQuickmodes(self) -> list[str]: 98 | available_quickmodes = [] 99 | for quickmode in ['comfort', 'eco', 'forcedLevelFour', 'holiday', 'standby', 'silent']: 100 | with suppress(PyViCareNotSupportedFeatureError): 101 | if self.service.getProperty(f"ventilation.quickmodes.{quickmode}") is not None: 102 | available_quickmodes.append(quickmode) 103 | return available_quickmodes 104 | 105 | @handleNotSupported 106 | def getVentilationQuickmode(self, quickmode: str) -> bool: 107 | return bool(self.service.getProperty(f"ventilation.quickmodes.{quickmode}")["properties"]["active"]["value"]) 108 | 109 | @handleNotSupported 110 | def activateVentilationQuickmode(self, quickmode: str) -> None: 111 | self.service.setProperty(f"ventilation.quickmodes.{quickmode}", "activate", {}) 112 | 113 | @handleNotSupported 114 | def deactivateVentilationQuickmode(self, quickmode: str) -> None: 115 | self.service.setProperty(f"ventilation.quickmodes.{quickmode}", "deactivate", {}) 116 | 117 | @handleNotSupported 118 | def getVentilationPrograms(self): 119 | available_programs = [] 120 | for program in ['basic', 'intensive', 'reduced', 'standard', 'standby', 'holidayAtHome', 'permanent']: 121 | with suppress(PyViCareNotSupportedFeatureError): 122 | if self.service.getProperty(f"ventilation.operating.programs.{program}") is not None: 123 | available_programs.append(program) 124 | return available_programs 125 | 126 | @handleNotSupported 127 | @deprecated(reason="renamed, use getVentilationPrograms", version="2.40.0") 128 | def getAvailablePrograms(self): 129 | return self.getVentilationPrograms() 130 | 131 | @handleNotSupported 132 | def getActiveVentilationProgram(self): 133 | return self.service.getProperty("ventilation.operating.programs.active")["properties"]["value"]["value"] 134 | 135 | @handleNotSupported 136 | @deprecated(reason="renamed, use getActiveVentilationProgram", version="2.40.0") 137 | def getActiveProgram(self): 138 | return self.getActiveVentilationProgram() 139 | 140 | def activateVentilationProgram(self, program): 141 | """ Activate a program 142 | NOTE 143 | DEVICE_COMMUNICATION_ERROR can just mean that the program is already on 144 | Parameters 145 | ---------- 146 | program : str 147 | 148 | Returns 149 | ------- 150 | result: json 151 | json representation of the answer 152 | """ 153 | return self.service.setProperty(f"ventilation.operating.programs.{program}", "activate", {}) 154 | 155 | @deprecated(reason="renamed, use activateVentilationProgram", version="2.40.0") 156 | def activateProgram(self, program): 157 | """ Activate a program 158 | NOTE 159 | DEVICE_COMMUNICATION_ERROR can just mean that the program is already on 160 | Parameters 161 | ---------- 162 | program : str 163 | 164 | Returns 165 | ------- 166 | result: json 167 | json representation of the answer 168 | """ 169 | return self.activateVentilationProgram(program) 170 | 171 | def deactivateVentilationProgram(self, program): 172 | """ Deactivate a program 173 | Parameters 174 | ---------- 175 | program : str 176 | 177 | Returns 178 | ------- 179 | result: json 180 | json representation of the answer 181 | """ 182 | return self.service.setProperty(f"ventilation.operating.programs.{program}", "deactivate", {}) 183 | 184 | @deprecated(reason="renamed, use deactivateVentilationProgram", version="2.40.0") 185 | def deactivateProgram(self, program): 186 | """ Deactivate a program 187 | Parameters 188 | ---------- 189 | program : str 190 | 191 | Returns 192 | ------- 193 | result: json 194 | json representation of the answer 195 | """ 196 | return self.deactivateVentilationProgram(program) 197 | 198 | @handleNotSupported 199 | def getVentilationSchedule(self): 200 | properties = self.service.getProperty("ventilation.schedule")["properties"] 201 | return { 202 | "active": properties["active"]["value"], 203 | "mon": properties["entries"]["value"]["mon"], 204 | "tue": properties["entries"]["value"]["tue"], 205 | "wed": properties["entries"]["value"]["wed"], 206 | "thu": properties["entries"]["value"]["thu"], 207 | "fri": properties["entries"]["value"]["fri"], 208 | "sat": properties["entries"]["value"]["sat"], 209 | "sun": properties["entries"]["value"]["sun"] 210 | } 211 | 212 | @handleNotSupported 213 | @deprecated(reason="renamed, use getVentilationSchedule", version="2.40.0") 214 | def getSchedule(self): 215 | return self.getVentilationSchedule() 216 | -------------------------------------------------------------------------------- /PyViCare/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openviess/PyViCare/e2e5f110c2e8143e65ef159142e33863404ee8c0/PyViCare/__init__.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyViCare 2 | 3 | This library implements access to Viessmann devices by using the official API from the [Viessmann Developer Portal](https://developer.viessmann.com/). 4 | 5 | ## Breaking changes in version 2.27.x 6 | 7 | - Some base classes have been renamed to provide a better support for non heating devices. See [PR #307](https://github.com/somm15/PyViCare/pull/307) 8 | 9 | ## Breaking changes in version 2.8.x 10 | 11 | - The circuit, burner (Gaz) and compressor (Heat Pump) is now separated. Accessing the properties of the burner/compressor is moved from `device.circuits` to `device.burners` and `device.compressor`. 12 | 13 | ## Breaking changes in version 2.x 14 | 15 | - The API to access your device changed to a general `PyViCare` class. Use this class to load all available devices. 16 | - The API to access the heating circuit of the device has moved to the `Device` class. You can now access and iterate over all available circuits via `device.circuits`. This allows to easily see which properties are depending on the circuit. 17 | 18 | See the example below for how you can use that. 19 | 20 | ## Breaking changes in version 1.x 21 | 22 | - The versions prior to 1.x used an unofficial API which stopped working on July, 15th 2021. All users need to migrate to version 1.0.0 to continue using the API. 23 | - Exception is raised if the library runs into a API rate limit. (See feature flag `raise_exception_on_rate_limit`) 24 | - Exception is raised if an unsupported device feature is used. (See feature flag `raise_exception_on_not_supported_device_feature`) 25 | - Python 3.4 is no longer supported. 26 | - Python 3.9 is now supported. 27 | 28 | ## Prerequisites 29 | 30 | To use PyViCare, every user has to register and create their personal API client. Follow these steps to create your client: 31 | 32 | 1. Login to the [Viessmann Developer Portal](https://app.developer.viessmann.com/) with **your existing ViCare app username/password**. 33 | 2. On the developer dashboard click *add* in the *clients* section. 34 | 3. Create a new client using following data: 35 | - Name: PyViCare 36 | - Google reCAPTCHA: Disabled 37 | - Redirect URIs: `vicare://oauth-callback/everest` 38 | 4. Copy the `Client ID` to use in your code. Pass it as constructor parameter to the device. 39 | 40 | Please note that not all properties from older versions and the ViCare mobile app are available in the new API. Missing properties were removed and might be added later if they are available again. 41 | 42 | ## Help 43 | 44 | We need help testing and improving PyViCare, since the maintainers only have specific types of heating systems. For bugs, questions or feature requests join the [PyViCare channel on Discord](https://discord.gg/aM3SqCD88f) or create an issue in this repository. 45 | 46 | ## Device Features / Errors 47 | 48 | Depending on the device, some features are not available/supported. This results in a raising of a `PyViCareNotSupportedFeatureError` if the dedicated method is called. This is most likely not a bug, but a limitation of the device itself. 49 | 50 | Tip: You can use Pythons [contextlib.suppress](https://docs.python.org/3/library/contextlib.html#contextlib.suppress) to handle it gracefully. 51 | 52 | ## Types of heatings 53 | 54 | - Use `asGazBoiler` for gas heatings 55 | - Use `asHeatPump` for heat pumps 56 | - Use `asFuelCell` for fuel cells 57 | - Use `asPelletsBoiler` for pellets heatings 58 | - Use `asOilBoiler` for oil heatings 59 | - Use `asHybridDevice` for gas/heat pump hybrid heatings 60 | 61 | ## Basic Usage: 62 | 63 | ```python 64 | import sys 65 | import logging 66 | from PyViCare.PyViCare import PyViCare 67 | 68 | client_id = "INSERT CLIENT ID" 69 | email = "email@domain" 70 | password = "password" 71 | 72 | vicare = PyViCare() 73 | vicare.initWithCredentials(email, password, client_id, "token.save") 74 | device = vicare.devices[0] 75 | print(device.getModel()) 76 | print("Online" if device.isOnline() else "Offline") 77 | 78 | t = device.asAutoDetectDevice() 79 | print(t.getDomesticHotWaterConfiguredTemperature()) 80 | print(t.getDomesticHotWaterStorageTemperature()) 81 | print(t.getOutsideTemperature()) 82 | print(t.getRoomTemperature()) 83 | print(t.getBoilerTemperature()) 84 | print(t.setDomesticHotWaterTemperature(59)) 85 | 86 | circuit = t.circuits[0] #select heating circuit 87 | 88 | print(circuit.getSupplyTemperature()) 89 | print(circuit.getHeatingCurveShift()) 90 | print(circuit.getHeatingCurveSlope()) 91 | 92 | print(circuit.getActiveProgram()) 93 | print(circuit.getPrograms()) 94 | 95 | print(circuit.getCurrentDesiredTemperature()) 96 | print(circuit.getDesiredTemperatureForProgram("comfort")) 97 | print(circuit.getActiveMode()) 98 | 99 | print(circuit.getDesiredTemperatureForProgram("comfort")) 100 | print(circuit.setProgramTemperature("comfort",21)) 101 | print(circuit.activateProgram("comfort")) 102 | print(circuit.deactivateComfort()) 103 | 104 | burner = t.burners[0] #select burner 105 | print(burner.getActive()) 106 | 107 | compressor = t.compressors[0] #select compressor 108 | print(compressor.getActive()) 109 | 110 | ``` 111 | 112 | ## API Usage in Postman 113 | 114 | Follow these steps to access the API in Postman: 115 | 116 | 1. Create an access token in the `Authorization` tab with type `OAuth 2.0` and following inputs: 117 | 118 | - Token Name: `PyViCare` 119 | - Grant Type: `Authorization Code (With PKCE)` 120 | - Callback URL: `vicare://oauth-callback/everest` 121 | - Authorize using browser: Disabled 122 | - Auth URL: `https://iam.viessmann.com/idp/v3/authorize` 123 | - Access Token URL: `https://iam.viessmann.com/idp/v3/token` 124 | - Client ID: Your personal Client ID created in the developer portal. 125 | - Client Secret: Blank 126 | - Code Challenge Method: `SHA-256` 127 | - Code Veriefier: Blank 128 | - Scope: `IoT User` 129 | - State: Blank 130 | - Client Authentication: `Send client credentials in body`. 131 | 132 | A login popup will open. Enter your ViCare username and password. 133 | 134 | 2. Use this URL to access your `installationId`, `gatewaySerial` and `deviceId`: 135 | 136 | `https://api.viessmann.com/iot/v1/equipment/installations?includeGateways=true` 137 | 138 | - `installationId` is `data[0].id` 139 | - `gatewaySerial` is `data[0].gateways[0].serial` 140 | - `deviceId` is `data[0].gateways[0].devices[0].id` 141 | 142 | 3. Use above data to replace `{installationId}`, `{gatewaySerial}` and `{deviceId}` in this URL to investigate the Viessmann API: 143 | 144 | `https://api.viessmann.com/iot/v1/features/installations/{installationId}/gateways/{gatewaySerial}/devices/{deviceId}/features` 145 | 146 | ## Rate Limits 147 | 148 | [Due to latest changes in the Viessmann API](https://www.viessmann-community.com/t5/Konnektivitaet/Q-amp-A-Viessmann-API/td-p/127660) rate limits can be hit. In that case a `PyViCareRateLimitError` is raised. You can read from the error (`limitResetDate`) when the rate limit is reset. 149 | 150 | ## More different devices for test cases needed 151 | 152 | In order to help ensuring making it easier to create more test cases you can run this code and make a pull request with the new test of your device type added. Your test should be committed into [tests/response](tests/response) and named ``. 153 | 154 | The code to run to make this happen is below. This automatically removes "sensitive" information like installation id and serial numbers. 155 | You can either replace default values or use the `PYVICARE_*` environment variables. 156 | 157 | ```python 158 | import sys 159 | import os 160 | from PyViCare.PyViCare import PyViCare 161 | 162 | client_id = os.getenv("PYVICARE_CLIENT_ID", "INSERT CLIENT_ID") 163 | email = os.getenv("PYVICARE_EMAIL", "email@domain") 164 | password = os.getenv("PYVICARE_PASSWORD", "password") 165 | 166 | vicare = PyViCare() 167 | vicare.initWithCredentials(email, password, client_id, "token.save") 168 | 169 | with open(f"dump.json", mode='w') as output: 170 | output.write(vicare.devices[0].dump_secure()) 171 | ``` 172 | 173 | To make the test data comparable with future updates, it must be sorted. No worries, this can be done automatically using [`jq`](https://jqlang.github.io/jq/). 174 | 175 | ```sh 176 | jq ".data|=sort_by(.feature)" --sort-keys testData.json > testDataSorted.json 177 | ``` 178 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | warn_return_any = True 4 | warn_unused_configs = True 5 | exclude = setup\.py$|build/ 6 | 7 | [mypy-requests_oauthlib.*] 8 | ignore_missing_imports = True 9 | 10 | [mypy-authlib.*] 11 | ignore_missing_imports = True 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "PyViCare" 3 | version = "0.1.0" 4 | description = "Library to communicate with the Viessmann ViCare API." 5 | authors = [ 6 | "Simon Gillet " 7 | ] 8 | maintainers = [ 9 | "Christopher Fenner ", 10 | "Martin", 11 | "Lukas Wöhrl", 12 | ] 13 | license = "Apache-2.0" 14 | readme = "README.md" 15 | homepage = "https://github.com/openviess/PyViCare" 16 | repository = "https://github.com/openviess/PyViCare" 17 | documentation = "https://github.com/openviess/PyViCare" 18 | keywords = [ 19 | "viessmann", 20 | "vicare", 21 | "api" 22 | ] 23 | classifiers = [ 24 | "Intended Audience :: Developers", 25 | "Natural Language :: English", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3", 31 | "License :: OSI Approved :: Apache Software License", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | "Operating System :: OS Independent", 34 | ] 35 | packages = [ 36 | { include = "PyViCare", from = "." }, 37 | ] 38 | 39 | [tool.poetry.urls] 40 | "Bug Tracker" = "https://github.com/openviess/PyViCare/issues" 41 | Changelog = "https://github.com/openviess/PyViCare/releases" 42 | 43 | [tool.poetry.dependencies] 44 | authlib = ">1.2.0" 45 | python = "^3.9" 46 | requests = ">=2.31.0" 47 | deprecated = "^1.2.15" 48 | 49 | [tool.poetry.group.dev.dependencies] 50 | codespell = "^2.3.0" 51 | mypy = "^1.11.2" 52 | pylint = "^3.2.6" 53 | pytest = "^8.3.2" 54 | pytest-cov = "^6.0.0" 55 | ruff = "^0.11.0" 56 | types-deprecated = "^1.2.15.20241117" 57 | types-requests = ">=2.31" 58 | 59 | [tool.mypy] 60 | # Specify the target platform details in config, so your developers are 61 | # free to run mypy on Windows, Linux, or macOS and get consistent 62 | # results. 63 | platform = "linux" 64 | python_version = "3.11" 65 | 66 | # show error messages from unrelated files 67 | follow_imports = "normal" 68 | 69 | # suppress errors about unsatisfied imports 70 | ignore_missing_imports = true 71 | 72 | # be strict 73 | check_untyped_defs = true 74 | disallow_any_generics = true 75 | disallow_incomplete_defs = true 76 | disallow_subclassing_any = true 77 | disallow_untyped_calls = true 78 | disallow_untyped_decorators = true 79 | disallow_untyped_defs = true 80 | no_implicit_optional = true 81 | strict_optional = true 82 | warn_incomplete_stub = true 83 | warn_no_return = true 84 | warn_redundant_casts = true 85 | warn_return_any = true 86 | warn_unused_configs = true 87 | warn_unused_ignores = true 88 | 89 | [[tool.mypy.overrides]] 90 | module = "authlib.*" 91 | ignore_missing_imports = true 92 | 93 | [tool.pylint."MESSAGES CONTROL"] 94 | disable = [ 95 | "duplicate-code", 96 | "fixme", 97 | "line-too-long", 98 | "invalid-name", 99 | "too-many-public-methods", 100 | "too-few-public-methods", 101 | # FIXME: 102 | "arguments-differ", 103 | "attribute-defined-outside-init", 104 | "bad-classmethod-argument", 105 | "chained-comparison", 106 | "consider-merging-isinstance", 107 | "consider-using-dict-items", 108 | "consider-using-generator", 109 | "deprecated-decorator", 110 | "missing-class-docstring", 111 | "missing-function-docstring", 112 | "missing-module-docstring", 113 | "missing-timeout", 114 | "raise-missing-from", 115 | "unspecified-encoding", 116 | "useless-object-inheritance", 117 | "useless-parent-delegation", 118 | ] 119 | 120 | [build-system] 121 | build-backend = "poetry.core.masonry.api" 122 | requires = ["poetry-core>=1.0.0"] 123 | -------------------------------------------------------------------------------- /tests/ViCareServiceMock.py: -------------------------------------------------------------------------------- 1 | from PyViCare.PyViCareService import (ViCareDeviceAccessor, 2 | buildSetPropertyUrl, readFeature) 3 | from tests.helper import readJson 4 | 5 | 6 | def MockCircuitsData(circuits): 7 | return { 8 | "properties": { 9 | "enabled": { 10 | "value": circuits 11 | } 12 | }, 13 | "feature": "heating.circuits", 14 | } 15 | 16 | 17 | class ViCareServiceMock: 18 | 19 | def __init__(self, filename, rawInput=None): 20 | if rawInput is None: 21 | testData = readJson(filename) 22 | self.testData = testData 23 | else: 24 | self.testData = rawInput 25 | 26 | self.accessor = ViCareDeviceAccessor( 27 | '[id]', '[serial]', '[deviceid]') 28 | self.setPropertyData = [] 29 | 30 | def getProperty(self, property_name): 31 | entities = self.testData["data"] 32 | return readFeature(entities, property_name) 33 | 34 | def setProperty(self, property_name, action, data): 35 | self.setPropertyData.append({ 36 | "url": buildSetPropertyUrl(self.accessor, property_name, action), 37 | "property_name": property_name, 38 | "action": action, 39 | "data": data 40 | }) 41 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | if __name__ == '__main__': 4 | unittest.main() 5 | -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from datetime import datetime 4 | from unittest.mock import patch 5 | 6 | from PyViCare.PyViCareUtils import ViCareTimer 7 | 8 | 9 | def readJson(fileName): 10 | test_filename = os.path.join(os.path.dirname(__file__), fileName) 11 | with open(test_filename, mode='rb') as json_file: 12 | return json.load(json_file) 13 | 14 | 15 | def enablePrintStatementsForTest(test_case): 16 | return test_case.capsys.disabled() 17 | 18 | 19 | def now_is(date_time): 20 | return patch.object(ViCareTimer, 'now', return_value=datetime.strptime(date_time, '%Y-%m-%d %H:%M:%S')) 21 | -------------------------------------------------------------------------------- /tests/response/TCU300_ethernet.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "feature": "gateway.devices", 7 | "gatewayId": "################", 8 | "isEnabled": true, 9 | "isReady": true, 10 | "properties": { 11 | "devices": { 12 | "type": "DeviceList", 13 | "value": [ 14 | { 15 | "fingerprint": "####", 16 | "id": "gateway", 17 | "modelId": "E3_TCU10_x07", 18 | "modelVersion": "####", 19 | "name": "TCU", 20 | "roles": [ 21 | "capability:hems", 22 | "capability:zigbeeCoordinator", 23 | "type:E3", 24 | "type:gateway;TCU300" 25 | ], 26 | "status": "online", 27 | "type": "tcu" 28 | }, 29 | { 30 | "fingerprint": "###", 31 | "id": "HEMS", 32 | "modelId": "E3_HEMS", 33 | "modelVersion": "###", 34 | "name": "Home Energy Management System", 35 | "roles": [ 36 | "type:E3", 37 | "type:virtual;hems" 38 | ], 39 | "status": "online", 40 | "type": "hems" 41 | }, 42 | { 43 | "fingerprint": "###", 44 | "id": "RoomControl-1", 45 | "modelId": "E3_RoomControl_One_525", 46 | "modelVersion": "####", 47 | "name": "E3_RoomControl_One_525", 48 | "roles": [ 49 | "capability:monetization;FTDC", 50 | "capability:monetization;OWD", 51 | "capability:zigbeeCoordinator", 52 | "type:E3", 53 | "type:virtual;smartRoomControl" 54 | ], 55 | "status": "online", 56 | "type": "roomControl" 57 | }, 58 | { 59 | "fingerprint": "#####", 60 | "id": "EEBUS", 61 | "modelId": "E3_EEBus", 62 | "modelVersion": "#####", 63 | "name": "accessories", 64 | "roles": [ 65 | "type:E3", 66 | "type:accessory;eeBus" 67 | ], 68 | "status": "online", 69 | "type": "EEBus" 70 | }, 71 | { 72 | "fingerprint": "ecu;#####", 73 | "id": "0", 74 | "modelId": "E3_VitoCharge_03", 75 | "modelVersion": "####", 76 | "name": "E3 device", 77 | "roles": [ 78 | "capability:hems", 79 | "type:E3", 80 | "type:ess", 81 | "type:photovoltaic;Internal", 82 | "type:product;Vitocharge" 83 | ], 84 | "status": "online", 85 | "type": "electricityStorage" 86 | }, 87 | { 88 | "fingerprint": "eebus:wallbox;########", 89 | "id": "eebus-1", 90 | "modelId": "E3_HEMS_VCS", 91 | "modelVersion": "####", 92 | "name": "Home Energy Management System", 93 | "roles": [ 94 | "type:E3", 95 | "type:accessory;vehicleChargingStation" 96 | ], 97 | "status": "online", 98 | "type": "vehicleChargingStation" 99 | } 100 | ] 101 | } 102 | }, 103 | "timestamp": "2024-03-17T18:55:46.182Z", 104 | "uri": "https://api.viessmann.com/iot/v1/features/installations/252756/gateways/################/features/gateway.devices" 105 | } 106 | ] 107 | } 108 | -------------------------------------------------------------------------------- /tests/response/VitochargeVX3.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "deviceId": "0", 7 | "feature": "device.serial", 8 | "gatewayId": "################", 9 | "isEnabled": true, 10 | "isReady": true, 11 | "properties": { 12 | "value": { 13 | "type": "string", 14 | "value": "################" 15 | } 16 | }, 17 | "timestamp": "2023-05-25T14:43:26.622Z", 18 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/device.serial" 19 | }, 20 | { 21 | "apiVersion": 1, 22 | "commands": {}, 23 | "deviceId": "0", 24 | "feature": "ess.operationState", 25 | "gatewayId": "################", 26 | "isEnabled": true, 27 | "isReady": true, 28 | "properties": { 29 | "value": { 30 | "type": "string", 31 | "value": "discharge" 32 | } 33 | }, 34 | "timestamp": "2023-05-26T18:33:10.937Z", 35 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/ess.operationState" 36 | }, 37 | { 38 | "apiVersion": 1, 39 | "commands": {}, 40 | "deviceId": "0", 41 | "feature": "ess.power", 42 | "gatewayId": "################", 43 | "isEnabled": true, 44 | "isReady": true, 45 | "properties": { 46 | "value": { 47 | "type": "number", 48 | "unit": "watt", 49 | "value": 700 50 | } 51 | }, 52 | "timestamp": "2023-05-26T20:21:12.542Z", 53 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/ess.power" 54 | }, 55 | { 56 | "apiVersion": 1, 57 | "commands": {}, 58 | "deviceId": "0", 59 | "feature": "ess.stateOfCharge", 60 | "gatewayId": "################", 61 | "isEnabled": true, 62 | "isReady": true, 63 | "properties": { 64 | "value": { 65 | "type": "number", 66 | "unit": "percent", 67 | "value": 91 68 | } 69 | }, 70 | "timestamp": "2023-05-26T20:20:30.651Z", 71 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/ess.stateOfCharge" 72 | }, 73 | { 74 | "apiVersion": 1, 75 | "commands": {}, 76 | "deviceId": "0", 77 | "feature": "ess.transfer.discharge.cumulated", 78 | "gatewayId": "################", 79 | "isEnabled": true, 80 | "isReady": true, 81 | "properties": { 82 | "currentDay": { 83 | "type": "number", 84 | "unit": "wattHour", 85 | "value": 4751 86 | }, 87 | "currentMonth": { 88 | "type": "number", 89 | "unit": "wattHour", 90 | "value": 66926 91 | }, 92 | "currentWeek": { 93 | "type": "number", 94 | "unit": "wattHour", 95 | "value": 29820 96 | }, 97 | "currentYear": { 98 | "type": "number", 99 | "unit": "wattHour", 100 | "value": 66926 101 | }, 102 | "lifeCycle": { 103 | "type": "number", 104 | "unit": "wattHour", 105 | "value": 66926 106 | } 107 | }, 108 | "timestamp": "2023-05-26T20:21:05.602Z", 109 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/ess.transfer.discharge.cumulated" 110 | }, 111 | { 112 | "apiVersion": 1, 113 | "commands": {}, 114 | "deviceId": "0", 115 | "feature": "heating.boiler.serial", 116 | "gatewayId": "################", 117 | "isEnabled": true, 118 | "isReady": true, 119 | "properties": { 120 | "value": { 121 | "type": "string", 122 | "value": "################" 123 | } 124 | }, 125 | "timestamp": "2023-05-25T14:43:26.623Z", 126 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/heating.boiler.serial" 127 | }, 128 | { 129 | "apiVersion": 1, 130 | "commands": {}, 131 | "deviceId": "0", 132 | "feature": "pcc.transfer.consumption.total", 133 | "gatewayId": "################", 134 | "isEnabled": true, 135 | "isReady": true, 136 | "properties": { 137 | "value": { 138 | "type": "number", 139 | "unit": "wattHour", 140 | "value": 7700 141 | } 142 | }, 143 | "timestamp": "2023-05-25T16:55:44.150Z", 144 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/pcc.transfer.consumption.total" 145 | }, 146 | { 147 | "apiVersion": 1, 148 | "commands": {}, 149 | "deviceId": "0", 150 | "feature": "pcc.transfer.feedIn.total", 151 | "gatewayId": "################", 152 | "isEnabled": true, 153 | "isReady": true, 154 | "properties": { 155 | "value": { 156 | "type": "number", 157 | "unit": "wattHour", 158 | "value": 298900 159 | } 160 | }, 161 | "timestamp": "2023-05-26T18:19:16.330Z", 162 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/pcc.transfer.feedIn.total" 163 | }, 164 | { 165 | "apiVersion": 1, 166 | "commands": {}, 167 | "deviceId": "0", 168 | "feature": "pcc.transfer.power.exchange", 169 | "gatewayId": "################", 170 | "isEnabled": true, 171 | "isReady": true, 172 | "properties": { 173 | "value": { 174 | "type": "number", 175 | "unit": "watt", 176 | "value": 0 177 | } 178 | }, 179 | "timestamp": "2023-05-26T20:21:05.602Z", 180 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/pcc.transfer.power.exchange" 181 | }, 182 | { 183 | "apiVersion": 1, 184 | "commands": {}, 185 | "deviceId": "0", 186 | "feature": "photovoltaic.production.cumulated", 187 | "gatewayId": "################", 188 | "isEnabled": true, 189 | "isReady": true, 190 | "properties": { 191 | "currentDay": { 192 | "type": "number", 193 | "unit": "wattHour", 194 | "value": 47440 195 | }, 196 | "currentMonth": { 197 | "type": "number", 198 | "unit": "wattHour", 199 | "value": 487670 200 | }, 201 | "currentWeek": { 202 | "type": "number", 203 | "unit": "wattHour", 204 | "value": 208436 205 | }, 206 | "currentYear": { 207 | "type": "number", 208 | "unit": "wattHour", 209 | "value": 487670 210 | }, 211 | "lifeCycle": { 212 | "type": "number", 213 | "unit": "wattHour", 214 | "value": 487670 215 | } 216 | }, 217 | "timestamp": "2023-05-26T19:22:09.055Z", 218 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/photovoltaic.production.cumulated" 219 | }, 220 | { 221 | "apiVersion": 1, 222 | "commands": {}, 223 | "deviceId": "0", 224 | "feature": "photovoltaic.production.current", 225 | "gatewayId": "################", 226 | "isEnabled": true, 227 | "isReady": true, 228 | "properties": { 229 | "value": { 230 | "type": "number", 231 | "unit": "kilowatt", 232 | "value": 0 233 | } 234 | }, 235 | "timestamp": "2023-05-26T19:19:13.292Z", 236 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/photovoltaic.production.current" 237 | }, 238 | { 239 | "apiVersion": 1, 240 | "commands": {}, 241 | "deviceId": "0", 242 | "feature": "photovoltaic.status", 243 | "gatewayId": "################", 244 | "isEnabled": true, 245 | "isReady": true, 246 | "properties": { 247 | "status": { 248 | "type": "string", 249 | "value": "ready" 250 | } 251 | }, 252 | "timestamp": "2023-05-26T19:10:36.637Z", 253 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/0/features/photovoltaic.status" 254 | } 255 | ] 256 | } 257 | -------------------------------------------------------------------------------- /tests/response/VitoconnectOpto1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "feature": "gateway.devices", 7 | "gatewayId": "################", 8 | "isEnabled": true, 9 | "isReady": true, 10 | "properties": { 11 | "devices": { 12 | "type": "DeviceList", 13 | "value": [ 14 | { 15 | "fingerprint": "xxx", 16 | "id": "gateway", 17 | "modelId": "Heatbox1", 18 | "modelVersion": "xxx", 19 | "name": "Heatbox 1, Vitoconnect", 20 | "roles": [ 21 | "type:gateway;VitoconnectOpto1", 22 | "type:legacy" 23 | ], 24 | "status": "online", 25 | "type": "vitoconnect" 26 | }, 27 | { 28 | "fingerprint": "xxx", 29 | "id": "0", 30 | "modelId": "VScotHO1_40", 31 | "modelVersion": "xxx", 32 | "name": "VT 200 (HO1A / HO1B)", 33 | "roles": [ 34 | "type:boiler", 35 | "type:legacy", 36 | "type:product;VScotHO1" 37 | ], 38 | "status": "online", 39 | "type": "heating" 40 | } 41 | ] 42 | } 43 | }, 44 | "timestamp": "2023-12-25T04:01:00.448Z", 45 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/features/gateway.devices" 46 | }, 47 | { 48 | "apiVersion": 1, 49 | "commands": {}, 50 | "feature": "gateway.wifi", 51 | "gatewayId": "################", 52 | "isEnabled": true, 53 | "isReady": true, 54 | "properties": { 55 | "strength": { 56 | "type": "number", 57 | "unit": "", 58 | "value": -69 59 | } 60 | }, 61 | "timestamp": "2023-12-26T20:44:41.417Z", 62 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/features/gateway.wifi" 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /tests/response/VitoconnectOpto2.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "feature": "gateway.devices", 7 | "gatewayId": "##############", 8 | "isEnabled": true, 9 | "isReady": true, 10 | "properties": { 11 | "devices": { 12 | "type": "DeviceList", 13 | "value": [ 14 | { 15 | "fingerprint": "###", 16 | "id": "gateway", 17 | "modelId": "Heatbox2_SRC", 18 | "modelVersion": "###", 19 | "name": "Heatbox 2 SRC, Vitoconnect", 20 | "roles": [ 21 | "type:gateway;VitoconnectOpto2/OT2", 22 | "type:hb2", 23 | "type:legacy" 24 | ], 25 | "status": "online", 26 | "type": "vitoconnect" 27 | }, 28 | { 29 | "fingerprint": "###", 30 | "id": "RoomControl-1", 31 | "modelId": "Smart_RoomControl", 32 | "modelVersion": "###", 33 | "name": "Smart_RoomControl_49", 34 | "roles": [ 35 | "capability:monetization;FTDC", 36 | "capability:monetization;OWD", 37 | "capability:zigbeeCoordinator", 38 | "type:legacy", 39 | "type:virtual;smartRoomControl" 40 | ], 41 | "status": "online", 42 | "type": "roomControl" 43 | }, 44 | { 45 | "fingerprint": "ext_hd_ctrl:hb2,mj:2,mi:50,p:3", 46 | "id": "HeatDemandControl", 47 | "modelId": "HeatDemandControl", 48 | "modelVersion": "849a43846dbfc44d4024d3709dddc041efc7c0e1", 49 | "name": "External Heat Demand Control", 50 | "roles": [ 51 | "type:legacy", 52 | "type:virtual;heatDemandControl" 53 | ], 54 | "status": "online", 55 | "type": "virtual" 56 | }, 57 | { 58 | "fingerprint": "gg:20,gk:4d,si:66,esi:65535", 59 | "id": "0", 60 | "modelId": "CU401B_S", 61 | "modelVersion": "712d6e32c9e295df60b8ab278580eb730f6b58ec", 62 | "name": "Vitocalxxx-S mit Vitotronic 200 (Typ WO1C)", 63 | "roles": [ 64 | "capability:monetization;AdvancedReport", 65 | "type:heatpump", 66 | "type:legacy", 67 | "type:product;CU401B" 68 | ], 69 | "status": "online", 70 | "type": "heating" 71 | }, 72 | { 73 | "fingerprint": "###", 74 | "id": "1", 75 | "modelId": "VPlusHO1_40", 76 | "modelVersion": "###", 77 | "name": "VT 200 (HO1A / HO1B)", 78 | "roles": [ 79 | "type:boiler", 80 | "type:legacy", 81 | "type:product;VPlusHO1" 82 | ], 83 | "status": "online", 84 | "type": "heating" 85 | }, 86 | { 87 | "fingerprint": "###", 88 | "id": "zigbee-#####", 89 | "modelId": "Smart_Device_eTRV_generic_50", 90 | "modelVersion": "ac746d50a111d3eb8fa54146c05971aa2bc5b5cc", 91 | "name": "Smart_Device_eTRV_generic_50", 92 | "roles": [ 93 | "type:actuator", 94 | "type:legacy", 95 | "type:radiator", 96 | "type:smartRoomDevice" 97 | ], 98 | "status": "online", 99 | "type": "zigbee" 100 | } 101 | ] 102 | } 103 | }, 104 | "timestamp": "2024-03-28T05:06:02.633Z", 105 | "uri": "https://api.viessmann.com/iot/v1/features/installations/######/gateways/##############/features/gateway.devices" 106 | }, 107 | { 108 | "apiVersion": 1, 109 | "commands": {}, 110 | "feature": "gateway.wifi", 111 | "gatewayId": "##############", 112 | "isEnabled": true, 113 | "isReady": true, 114 | "properties": { 115 | "strength": { 116 | "type": "number", 117 | "unit": "", 118 | "value": -41 119 | } 120 | }, 121 | "timestamp": "2024-03-30T17:31:57.758Z", 122 | "uri": "https://api.viessmann.com/iot/v1/features/installations/######/gateways/##############/features/gateway.wifi" 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /tests/response/deviceerrors/F.1100.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "deviceId": "0", 7 | "feature": "device.messages.errors.raw", 8 | "gatewayId": "################", 9 | "isEnabled": true, 10 | "isReady": true, 11 | "properties": { 12 | "entries": { 13 | "type": "array", 14 | "value": [ 15 | { 16 | "accessLevel": "customer", 17 | "audiences": [ 18 | "IS-SUPPLIER", 19 | "IS-DEVELOPMENT", 20 | "IS-MANUFACTURING", 21 | "IS-AFTERSALES", 22 | "IS-AFTERMARKET", 23 | "IS-DEVELOPER-VEG", 24 | "IS-BIG-DATA", 25 | "IS-MANUFACTURING-VEG" 26 | ], 27 | "errorCode": "F.1100", 28 | "priority": "criticalError", 29 | "timestamp": "2000-07-22T20:37:44.000Z" 30 | } 31 | ] 32 | } 33 | }, 34 | "timestamp": "2024-10-30T08:53:23.913Z", 35 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/0/features/device.messages.errors.raw" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /tests/response/errors/error_500.json: -------------------------------------------------------------------------------- 1 | { 2 | "viErrorId": "req-dd5644cdb8114bc6b023b5d021a212bc", 3 | "statusCode": 500, 4 | "errorType": "INTERNAL_ERROR", 5 | "message": "Internal server error" 6 | } 7 | -------------------------------------------------------------------------------- /tests/response/errors/error_502.json: -------------------------------------------------------------------------------- 1 | { 2 | "viErrorId": "req-xxxxx", 3 | "statusCode": 502, 4 | "errorType": "DEVICE_COMMUNICATION_ERROR", 5 | "message": "DEVICE_COMMUNICATION_ERROR", 6 | "extendedPayload": { 7 | "reason": "INTERNAL_SERVER_ERROR" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/response/errors/expired_token.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "EXPIRED TOKEN" 3 | } 4 | -------------------------------------------------------------------------------- /tests/response/errors/gateway_offline.json: -------------------------------------------------------------------------------- 1 | { 2 | "viErrorId": "***", 3 | "statusCode": 400, 4 | "errorType": "DEVICE_COMMUNICATION_ERROR", 5 | "message": "", 6 | "extendedPayload": { 7 | "httpStatusCode": "NotFound", 8 | "code": "404", 9 | "reason": "GATEWAY_OFFLINE" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/response/errors/rate_limit.json: -------------------------------------------------------------------------------- 1 | { 2 | "errorType": "RATE_LIMIT_EXCEEDED", 3 | "extendedPayload": { 4 | "clientId": "XXXX", 5 | "limitReset": 1584462010106, 6 | "name": "ViCare day limit", 7 | "requestCountLimit": 1450, 8 | "userId": "XXXX" 9 | }, 10 | "message": "API calls rate limit has been exceeded. Please wait until your limit will renew.", 11 | "statusCode": 429, 12 | "viErrorId": "XXX" 13 | } 14 | -------------------------------------------------------------------------------- /tests/response/zigbee_Smart_Device_eTRV_generic_50.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "deviceId": "zigbee-048727fffeb429ae", 7 | "feature": "device.messages.errors.raw", 8 | "gatewayId": "################", 9 | "isEnabled": true, 10 | "isReady": true, 11 | "properties": { 12 | "entries": { 13 | "type": "array", 14 | "value": [ 15 | { 16 | "accessLevel": "customer", 17 | "audiences": [], 18 | "errorCode": "F.731", 19 | "priority": "criticalError" 20 | } 21 | ] 22 | } 23 | }, 24 | "timestamp": "2025-02-03T02:30:52.129Z", 25 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffeb429ae/features/device.messages.errors.raw" 26 | }, 27 | { 28 | "apiVersion": 1, 29 | "commands": { 30 | "setName": { 31 | "isExecutable": true, 32 | "name": "setName", 33 | "params": { 34 | "name": { 35 | "constraints": { 36 | "maxLength": 40, 37 | "minLength": 1, 38 | "regEx": "^[\\p{L}0-9]+( [\\p{L}0-9]+)*$" 39 | }, 40 | "required": true, 41 | "type": "string" 42 | } 43 | }, 44 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffeb429ae/features/device.name/commands/setName" 45 | } 46 | }, 47 | "deviceId": "zigbee-048727fffeb429ae", 48 | "feature": "device.name", 49 | "gatewayId": "################", 50 | "isEnabled": true, 51 | "isReady": true, 52 | "properties": { 53 | "name": { 54 | "type": "string", 55 | "value": "Wohnung" 56 | } 57 | }, 58 | "timestamp": "2025-02-03T02:30:52.129Z", 59 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffeb429ae/features/device.name" 60 | }, 61 | { 62 | "apiVersion": 1, 63 | "commands": {}, 64 | "deviceId": "zigbee-048727fffeb429ae", 65 | "feature": "device.power.battery", 66 | "gatewayId": "################", 67 | "isEnabled": true, 68 | "isReady": true, 69 | "properties": { 70 | "level": { 71 | "type": "number", 72 | "unit": "percent", 73 | "value": 66 74 | } 75 | }, 76 | "timestamp": "2025-02-03T02:30:52.129Z", 77 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffeb429ae/features/device.power.battery" 78 | }, 79 | { 80 | "apiVersion": 1, 81 | "commands": {}, 82 | "deviceId": "zigbee-048727fffeb429ae", 83 | "feature": "device.sensors.temperature", 84 | "gatewayId": "################", 85 | "isEnabled": true, 86 | "isReady": true, 87 | "properties": { 88 | "status": { 89 | "type": "string", 90 | "value": "connected" 91 | }, 92 | "value": { 93 | "type": "number", 94 | "unit": "celsius", 95 | "value": 16.5 96 | } 97 | }, 98 | "timestamp": "2025-02-03T14:14:15.095Z", 99 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffeb429ae/features/device.sensors.temperature" 100 | }, 101 | { 102 | "apiVersion": 1, 103 | "commands": { 104 | "setTargetTemperature": { 105 | "isExecutable": true, 106 | "name": "setTargetTemperature", 107 | "params": { 108 | "temperature": { 109 | "constraints": { 110 | "max": 30, 111 | "min": 8, 112 | "stepping": 0.5 113 | }, 114 | "required": true, 115 | "type": "number" 116 | } 117 | }, 118 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffeb429ae/features/trv.temperature/commands/setTargetTemperature" 119 | } 120 | }, 121 | "deviceId": "zigbee-048727fffeb429ae", 122 | "feature": "trv.temperature", 123 | "gatewayId": "################", 124 | "isEnabled": true, 125 | "isReady": true, 126 | "properties": { 127 | "value": { 128 | "type": "number", 129 | "unit": "celsius", 130 | "value": 8 131 | } 132 | }, 133 | "timestamp": "2025-02-03T13:57:19.872Z", 134 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffeb429ae/features/trv.temperature" 135 | } 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /tests/response/zigbee_Smart_cs_generic_50.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "deviceId": "zigbee-f082c0fffe43d8cd", 7 | "feature": "device.messages.errors.raw", 8 | "gatewayId": "################", 9 | "isEnabled": true, 10 | "isReady": true, 11 | "properties": { 12 | "entries": { 13 | "type": "array", 14 | "value": [] 15 | } 16 | }, 17 | "timestamp": "2025-02-03T02:30:52.279Z", 18 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-f082c0fffe43d8cd/features/device.messages.errors.raw" 19 | }, 20 | { 21 | "apiVersion": 1, 22 | "commands": { 23 | "setName": { 24 | "isExecutable": true, 25 | "name": "setName", 26 | "params": { 27 | "name": { 28 | "constraints": { 29 | "maxLength": 40, 30 | "minLength": 1, 31 | "regEx": "^[\\p{L}0-9]+( [\\p{L}0-9]+)*$" 32 | }, 33 | "required": true, 34 | "type": "string" 35 | } 36 | }, 37 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-f082c0fffe43d8cd/features/device.name/commands/setName" 38 | } 39 | }, 40 | "deviceId": "zigbee-f082c0fffe43d8cd", 41 | "feature": "device.name", 42 | "gatewayId": "################", 43 | "isEnabled": true, 44 | "isReady": true, 45 | "properties": { 46 | "name": { 47 | "type": "string", 48 | "value": "stube oben" 49 | } 50 | }, 51 | "timestamp": "2025-02-03T02:30:52.279Z", 52 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-f082c0fffe43d8cd/features/device.name" 53 | }, 54 | { 55 | "apiVersion": 1, 56 | "commands": {}, 57 | "deviceId": "zigbee-f082c0fffe43d8cd", 58 | "feature": "device.power.battery", 59 | "gatewayId": "################", 60 | "isEnabled": true, 61 | "isReady": true, 62 | "properties": { 63 | "level": { 64 | "type": "number", 65 | "unit": "percent", 66 | "value": 89 67 | } 68 | }, 69 | "timestamp": "2025-02-03T02:30:52.279Z", 70 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-f082c0fffe43d8cd/features/device.power.battery" 71 | }, 72 | { 73 | "apiVersion": 1, 74 | "commands": {}, 75 | "deviceId": "zigbee-f082c0fffe43d8cd", 76 | "feature": "device.sensors.humidity", 77 | "gatewayId": "################", 78 | "isEnabled": true, 79 | "isReady": true, 80 | "properties": { 81 | "status": { 82 | "type": "string", 83 | "value": "connected" 84 | }, 85 | "value": { 86 | "type": "number", 87 | "unit": "percent", 88 | "value": 37 89 | } 90 | }, 91 | "timestamp": "2025-02-03T14:04:41.737Z", 92 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-f082c0fffe43d8cd/features/device.sensors.humidity" 93 | }, 94 | { 95 | "apiVersion": 1, 96 | "commands": {}, 97 | "deviceId": "zigbee-f082c0fffe43d8cd", 98 | "feature": "device.sensors.temperature", 99 | "gatewayId": "################", 100 | "isEnabled": true, 101 | "isReady": true, 102 | "properties": { 103 | "status": { 104 | "type": "string", 105 | "value": "connected" 106 | }, 107 | "value": { 108 | "type": "number", 109 | "unit": "celsius", 110 | "value": 15 111 | } 112 | }, 113 | "timestamp": "2025-02-03T14:17:36.528Z", 114 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-f082c0fffe43d8cd/features/device.sensors.temperature" 115 | } 116 | ] 117 | } 118 | -------------------------------------------------------------------------------- /tests/response/zigbee_zk03839.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "deviceId": "zigbee-2c1165fffe977770", 7 | "feature": "device.sensors.humidity", 8 | "gatewayId": "################", 9 | "isEnabled": true, 10 | "isReady": true, 11 | "properties": { 12 | "status": { 13 | "type": "string", 14 | "value": "connected" 15 | }, 16 | "value": { 17 | "type": "number", 18 | "unit": "percent", 19 | "value": 56 20 | } 21 | }, 22 | "timestamp": "2023-01-05T20:20:44.130Z", 23 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/zigbee-2c1165fffe977770/features/device.sensors.humidity" 24 | }, 25 | { 26 | "apiVersion": 1, 27 | "commands": {}, 28 | "deviceId": "zigbee-2c1165fffe977770", 29 | "feature": "device.sensors.temperature", 30 | "gatewayId": "################", 31 | "isEnabled": true, 32 | "isReady": true, 33 | "properties": { 34 | "status": { 35 | "type": "string", 36 | "value": "connected" 37 | }, 38 | "value": { 39 | "type": "number", 40 | "unit": "celsius", 41 | "value": 19.7 42 | } 43 | }, 44 | "timestamp": "2023-01-05T21:43:19.578Z", 45 | "uri": "https://api.viessmann.com/iot/v1/equipment/installations/#######/gateways/################/devices/zigbee-2c1165fffe977770/features/device.sensors.temperature" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /tests/response/zigbee_zk03840_trv.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "deviceId": "zigbee-048727fffe196e03", 7 | "feature": "device.messages.errors.raw", 8 | "gatewayId": "################", 9 | "isEnabled": true, 10 | "isReady": true, 11 | "properties": { 12 | "entries": { 13 | "type": "array", 14 | "value": [] 15 | } 16 | }, 17 | "timestamp": "2024-10-01T00:31:25.906Z", 18 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffe196e03/features/device.messages.errors.raw" 19 | }, 20 | { 21 | "apiVersion": 1, 22 | "commands": { 23 | "setName": { 24 | "isExecutable": true, 25 | "name": "setName", 26 | "params": { 27 | "name": { 28 | "constraints": { 29 | "maxLength": 40, 30 | "minLength": 1, 31 | "regEx": "^[\\p{L}0-9]+( [\\p{L}0-9]+)*$" 32 | }, 33 | "required": true, 34 | "type": "string" 35 | } 36 | }, 37 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffe196e03/features/device.name/commands/setName" 38 | } 39 | }, 40 | "deviceId": "zigbee-048727fffe196e03", 41 | "feature": "device.name", 42 | "gatewayId": "################", 43 | "isEnabled": true, 44 | "isReady": true, 45 | "properties": { 46 | "name": { 47 | "type": "string", 48 | "value": "" 49 | } 50 | }, 51 | "timestamp": "2024-10-01T00:31:25.906Z", 52 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffe196e03/features/device.name" 53 | }, 54 | { 55 | "apiVersion": 1, 56 | "commands": {}, 57 | "deviceId": "zigbee-048727fffe196e03", 58 | "feature": "device.sensors.temperature", 59 | "gatewayId": "################", 60 | "isEnabled": true, 61 | "isReady": true, 62 | "properties": { 63 | "status": { 64 | "type": "string", 65 | "value": "connected" 66 | }, 67 | "value": { 68 | "type": "number", 69 | "unit": "celsius", 70 | "value": 18.4 71 | } 72 | }, 73 | "timestamp": "2024-10-01T15:31:33.915Z", 74 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffe196e03/features/device.sensors.temperature" 75 | }, 76 | { 77 | "apiVersion": 1, 78 | "commands": { 79 | "setTargetTemperature": { 80 | "isExecutable": false, 81 | "name": "setTargetTemperature", 82 | "params": { 83 | "temperature": { 84 | "constraints": { 85 | "max": 30, 86 | "min": 8, 87 | "stepping": 0.5 88 | }, 89 | "required": true, 90 | "type": "number" 91 | } 92 | }, 93 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffe196e03/features/trv.temperature/commands/setTargetTemperature" 94 | } 95 | }, 96 | "deviceId": "zigbee-048727fffe196e03", 97 | "feature": "trv.temperature", 98 | "gatewayId": "################", 99 | "isEnabled": true, 100 | "isReady": true, 101 | "properties": { 102 | "value": { 103 | "type": "number", 104 | "unit": "celsius", 105 | "value": 8 106 | } 107 | }, 108 | "timestamp": "2024-10-01T08:44:50.292Z", 109 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-048727fffe196e03/features/trv.temperature" 110 | } 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /tests/response/zigbee_zk05390_repeater.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "apiVersion": 1, 5 | "commands": {}, 6 | "deviceId": "zigbee-142d41fffe8797bd", 7 | "feature": "device.messages.errors.raw", 8 | "gatewayId": "################", 9 | "isEnabled": true, 10 | "isReady": true, 11 | "properties": { 12 | "entries": { 13 | "type": "array", 14 | "value": [] 15 | } 16 | }, 17 | "timestamp": "2024-10-01T00:31:25.951Z", 18 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-142d41fffe8797bd/features/device.messages.errors.raw" 19 | }, 20 | { 21 | "apiVersion": 1, 22 | "commands": { 23 | "setName": { 24 | "isExecutable": true, 25 | "name": "setName", 26 | "params": { 27 | "name": { 28 | "constraints": { 29 | "maxLength": 40, 30 | "minLength": 1, 31 | "regEx": "^[\\p{L}0-9]+( [\\p{L}0-9]+)*$" 32 | }, 33 | "required": true, 34 | "type": "string" 35 | } 36 | }, 37 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-142d41fffe8797bd/features/device.name/commands/setName" 38 | } 39 | }, 40 | "deviceId": "zigbee-142d41fffe8797bd", 41 | "feature": "device.name", 42 | "gatewayId": "################", 43 | "isEnabled": true, 44 | "isReady": true, 45 | "properties": { 46 | "name": { 47 | "type": "string", 48 | "value": "Erdgeschoss" 49 | } 50 | }, 51 | "timestamp": "2024-10-01T00:31:25.951Z", 52 | "uri": "https://api.viessmann.com/iot/v1/features/installations/#######/gateways/################/devices/zigbee-142d41fffe8797bd/features/device.name" 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /tests/test_E3_TCU300_ethernet.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGateway import Gateway 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class TCU300_ethernet(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/TCU300_ethernet.json') 11 | self.device = Gateway(self.service) 12 | 13 | def test_getSerial(self): 14 | self.assertEqual( 15 | self.device.getSerial(), "################") 16 | 17 | def test_getWifiSignalStrength(self): 18 | with self.assertRaises(PyViCareNotSupportedFeatureError): 19 | self.device.getWifiSignalStrength() 20 | -------------------------------------------------------------------------------- /tests/test_Ecotronic.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCarePelletsBoiler import PelletsBoiler 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Ecotronic(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Ecotronic.json') 11 | self.device = PelletsBoiler(self.service) 12 | 13 | def test_isDomesticHotWaterDevice(self): 14 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 15 | 16 | def test_isSolarThermalDevice(self): 17 | self.assertEqual(self.device.isSolarThermalDevice(), False) 18 | 19 | def test_isVentilationDevice(self): 20 | self.assertEqual(self.device.isVentilationDevice(), False) 21 | 22 | def test_getBurnerStarts(self): 23 | self.assertEqual(self.device.burners[0].getStarts(), 2162) 24 | 25 | def test_getBurnerHours(self): 26 | self.assertEqual(self.device.burners[0].getHours(), 5648) 27 | 28 | def test_getActive(self): 29 | self.assertEqual(self.device.burners[0].getActive(), True) 30 | 31 | def test_getReturnTemperature(self): 32 | self.assertEqual( 33 | self.device.getReturnTemperature(), 60.3) 34 | 35 | def test_getBufferTopTemperature(self): 36 | self.assertEqual(self.device.getBufferTopTemperature(), 62.1) 37 | 38 | def test_getBufferMidTopTemperature(self): 39 | self.assertEqual(self.device.getBufferMidTopTemperature(), 50.7) 40 | 41 | def test_getBufferMiddleTemperature(self): 42 | with self.assertRaises(PyViCareNotSupportedFeatureError): 43 | self.device.getBufferMiddleTemperature() 44 | 45 | def test_getBufferMidBottomTemperature(self): 46 | self.assertEqual(self.device.getBufferMidBottomTemperature(), 44.6) 47 | 48 | def test_getBufferBottomTemperature(self): 49 | self.assertEqual(self.device.getBufferBottomTemperature(), 43.7) 50 | 51 | def test_getFuelNeed(self): 52 | self.assertEqual(self.device.getFuelNeed(), 17402) 53 | 54 | def test_getFuelUnit(self): 55 | self.assertEqual(self.device.getFuelUnit(), "kg") 56 | 57 | def test_getAshLevel(self): 58 | self.assertEqual(self.device.getAshLevel(), 43.7) 59 | -------------------------------------------------------------------------------- /tests/test_GenericDevice.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatingDevice import HeatingDevice 4 | from tests.ViCareServiceMock import MockCircuitsData, ViCareServiceMock 5 | 6 | 7 | class GenericDeviceTest(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock( 10 | None, {'data': [MockCircuitsData([0])]}) 11 | self.device = HeatingDevice(self.service) 12 | 13 | def test_activateComfort(self): 14 | self.device.circuits[0].activateComfort() 15 | self.assertEqual(len(self.service.setPropertyData), 1) 16 | self.assertEqual(self.service.setPropertyData[0]['action'], 'activate') 17 | self.assertEqual( 18 | self.service.setPropertyData[0]['property_name'], 'heating.circuits.0.operating.programs.comfort') 19 | 20 | def test_deactivateComfort(self): 21 | self.device.circuits[0].deactivateComfort() 22 | self.assertEqual(len(self.service.setPropertyData), 1) 23 | self.assertEqual( 24 | self.service.setPropertyData[0]['action'], 'deactivate') 25 | self.assertEqual( 26 | self.service.setPropertyData[0]['property_name'], 'heating.circuits.0.operating.programs.comfort') 27 | 28 | def test_setDomesticHotWaterTemperature(self): 29 | self.device.setDomesticHotWaterTemperature(50) 30 | self.assertEqual(len(self.service.setPropertyData), 1) 31 | self.assertEqual( 32 | self.service.setPropertyData[0]['property_name'], 'heating.dhw.temperature.main') 33 | self.assertEqual( 34 | self.service.setPropertyData[0]['action'], 'setTargetTemperature') 35 | self.assertEqual(self.service.setPropertyData[0]['data'], { 36 | 'temperature': 50}) 37 | 38 | def test_setMode(self): 39 | self.device.circuits[0].setMode('dhw') 40 | self.assertEqual(len(self.service.setPropertyData), 1) 41 | self.assertEqual( 42 | self.service.setPropertyData[0]['property_name'], 'heating.circuits.0.operating.modes.active') 43 | self.assertEqual(self.service.setPropertyData[0]['action'], 'setMode') 44 | self.assertEqual( 45 | self.service.setPropertyData[0]['data'], {'mode': 'dhw'}) 46 | 47 | def test_setHeatingCurve(self): 48 | self.device.circuits[0].setHeatingCurve(-2, 0.9) 49 | self.assertEqual(len(self.service.setPropertyData), 1) 50 | self.assertEqual( 51 | self.service.setPropertyData[0]['property_name'], 'heating.circuits.0.heating.curve') 52 | self.assertEqual( 53 | self.service.setPropertyData[0]['action'], 'setCurve') 54 | self.assertEqual(self.service.setPropertyData[0]['data'], {'shift': -2, 'slope': 0.9}) 55 | -------------------------------------------------------------------------------- /tests/test_Integration.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | 5 | import pytest 6 | 7 | from PyViCare.PyViCare import PyViCare 8 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 9 | from tests.helper import enablePrintStatementsForTest 10 | 11 | EXEC_INTEGRATION_TEST = int(os.getenv('EXEC_INTEGRATION_TEST', '0')) 12 | TOKEN_FILE = "browser.save" 13 | 14 | 15 | def all_getter_methods(obj): 16 | for method_name in dir(obj): 17 | if method_name.startswith("get"): 18 | method = getattr(obj, method_name) 19 | if callable(method): 20 | yield (method_name, method) 21 | 22 | 23 | def pretty_print_results(result): 24 | # format dictionary and lists nicely 25 | if isinstance(result, dict) or isinstance(result, list): 26 | formatted = json.dumps(result, sort_keys=True, indent=2) 27 | indented = formatted.replace('\n', '\n' + ' ' * 45) 28 | return indented 29 | return result 30 | 31 | 32 | def dump_results(vicare_device): 33 | for (name, method) in all_getter_methods(vicare_device): 34 | result = None 35 | try: 36 | result = pretty_print_results(method()) 37 | except TypeError: # skip methods which have more than one argument 38 | result = "Skipped" 39 | except PyViCareNotSupportedFeatureError: 40 | result = "Not Supported" 41 | print(f"{name:<45}{result}") 42 | 43 | 44 | def create_client(): 45 | client_id = os.getenv('PYVICARE_CLIENT_ID', '') 46 | 47 | vicare = PyViCare() 48 | vicare.initWithBrowserOAuth(client_id, TOKEN_FILE) 49 | return vicare 50 | 51 | 52 | class Integration(unittest.TestCase): 53 | @pytest.fixture(autouse=True) 54 | def capsys(self, capsys): 55 | self.capsys = capsys 56 | 57 | @unittest.skipIf(not EXEC_INTEGRATION_TEST, "environments needed") 58 | def test_PyViCare(self): 59 | with enablePrintStatementsForTest(self): 60 | print() 61 | 62 | vicare = create_client() 63 | 64 | print(f"Found {len(vicare.devices)} devices") 65 | 66 | for device_config in vicare.devices: 67 | print() 68 | print(f"{'model':<45}{device_config.getModel()}") 69 | print(f"{'isOnline':<45}{device_config.isOnline()}") 70 | 71 | device = device_config.asAutoDetectDevice() 72 | auto_type_name = type(device).__name__ 73 | print(f"{'detected type':<45}{auto_type_name}") 74 | 75 | print(f"{'Roles':<45}{', '.join(device.service.roles)})") 76 | 77 | dump_results(device) 78 | print() 79 | 80 | for circuit in device.circuits: 81 | print(f"{'Use circuit':<45}{circuit.id}") 82 | dump_results(circuit) 83 | print() 84 | 85 | for burner in device.burners: 86 | print(f"{'Use burner':<45}{burner.id}") 87 | dump_results(burner) 88 | print() 89 | 90 | for compressor in device.compressors: 91 | print(f"{'Use compressor':<45}{compressor.id}") 92 | dump_results(compressor) 93 | print() 94 | 95 | print() 96 | 97 | for i in vicare.installations: 98 | print(i.id) 99 | print(i.description) 100 | print(i.address.street) 101 | print() 102 | for g in i.gateways: 103 | print(g.producedAt) 104 | print(g.autoUpdate) 105 | print(g.aggregatedStatus) 106 | print(g.registeredAt) 107 | print() 108 | for d in g.devices: 109 | print(d.modelId) 110 | print(d.createdAt) 111 | 112 | @unittest.skipIf(not EXEC_INTEGRATION_TEST, "environments needed") 113 | def test_dump(self): 114 | with enablePrintStatementsForTest(self): 115 | vicare = vicare = create_client() 116 | 117 | with open("dump.json", mode='w', encoding="utf-8") as output: 118 | output.write(vicare.devices[0].dump_secure()) 119 | 120 | with open("dump.flat.json", mode='w', encoding="utf-8") as output: 121 | output.write(vicare.devices[0].dump_secure(flat=True)) 122 | -------------------------------------------------------------------------------- /tests/test_PyViCareCachedService.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | 4 | from PyViCare.PyViCareCachedService import ViCareCachedService 5 | from PyViCare.PyViCareService import ViCareDeviceAccessor 6 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 7 | from tests.helper import now_is 8 | 9 | 10 | class PyViCareCachedServiceTest(unittest.TestCase): 11 | 12 | CACHE_DURATION = 60 13 | 14 | def setUp(self): 15 | self.oauth_mock = Mock() 16 | self.oauth_mock.get.return_value = {'data': [{"feature": "someprop"}]} 17 | accessor = ViCareDeviceAccessor("[id]", "[serial]", "[device]") 18 | self.service = ViCareCachedService( 19 | self.oauth_mock, accessor, [], self.CACHE_DURATION) 20 | 21 | def test_getProperty_existing(self): 22 | self.service.getProperty("someprop") 23 | self.oauth_mock.get.assert_called_once_with( 24 | '/features/installations/[id]/gateways/[serial]/devices/[device]/features/') 25 | 26 | def test_getProperty_nonexisting_raises_exception(self): 27 | 28 | def func(): 29 | return self.service.getProperty("some-non-prop") 30 | self.assertRaises(PyViCareNotSupportedFeatureError, func) 31 | 32 | def test_setProperty_works(self): 33 | self.service.setProperty("someotherprop", "doaction", {'name': 'abc'}) 34 | self.oauth_mock.post.assert_called_once_with( 35 | '/features/installations/[id]/gateways/[serial]/devices/[device]/features/someotherprop/commands/doaction', '{"name": "abc"}') 36 | 37 | def test_getProperty_existing_cached(self): 38 | # time+0 seconds 39 | with now_is('2000-01-01 00:00:00'): 40 | self.service.getProperty("someprop") 41 | self.service.getProperty("someprop") 42 | 43 | # time+30 seconds 44 | with now_is('2000-01-01 00:00:30'): 45 | self.service.getProperty("someprop") 46 | 47 | self.assertEqual(self.oauth_mock.get.call_count, 1) 48 | self.oauth_mock.get.assert_called_once_with( 49 | '/features/installations/[id]/gateways/[serial]/devices/[device]/features/') 50 | 51 | # time+70 seconds (must be more than CACHE_DURATION) 52 | with now_is('2000-01-01 00:01:10'): 53 | self.service.getProperty("someprop") 54 | 55 | self.assertEqual(self.oauth_mock.get.call_count, 2) 56 | 57 | def test_setProperty_invalidateCache(self): 58 | # freeze time 59 | with now_is('2000-01-01 00:00:00'): 60 | self.assertEqual(self.service.is_cache_invalid(), True) 61 | self.service.getProperty("someprop") 62 | self.assertEqual(self.service.is_cache_invalid(), False) 63 | 64 | self.service.setProperty( 65 | "someotherprop", "doaction", {'name': 'abc'}) 66 | self.assertEqual(self.service.is_cache_invalid(), True) 67 | 68 | self.service.getProperty("someprop") 69 | self.assertEqual(self.oauth_mock.get.call_count, 2) 70 | -------------------------------------------------------------------------------- /tests/test_PyViCareExceptions.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | from PyViCare.PyViCareUtils import PyViCareCommandError, PyViCareRateLimitError 5 | from tests.helper import readJson 6 | 7 | 8 | class TestPyViCareRateLimitError(unittest.TestCase): 9 | 10 | def test_createFromResponse(self): 11 | mockResponse = readJson('response/errors/rate_limit.json') 12 | 13 | error = PyViCareRateLimitError(mockResponse) 14 | 15 | self.assertEqual( 16 | error.message, 'API rate limit ViCare day limit exceeded. Max 1450 calls in timewindow. Limit reset at 2020-03-17T16:20:10.106000.') 17 | self.assertEqual(error.limitResetDate, datetime.datetime( 18 | 2020, 3, 17, 16, 20, 10, 106000)) 19 | 20 | 21 | class TestPyViCareCommandError(unittest.TestCase): 22 | 23 | def test_createFromResponse(self): 24 | mockResponse = readJson('response/errors/error_502.json') 25 | 26 | error = PyViCareCommandError(mockResponse) 27 | 28 | self.assertEqual( 29 | error.message, 'Command failed with status code 502. Reason given was: INTERNAL_SERVER_ERROR') 30 | -------------------------------------------------------------------------------- /tests/test_PyViCareService.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | 4 | from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService 5 | 6 | 7 | class PyViCareServiceTest(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.oauth_mock = Mock() 11 | self.accessor = ViCareDeviceAccessor("[id]", "[serial]", "[device]") 12 | self.service = ViCareService(self.oauth_mock, self.accessor, []) 13 | 14 | def test_getProperty(self): 15 | self.service.getProperty("someprop") 16 | self.oauth_mock.get.assert_called_once_with( 17 | '/features/installations/[id]/gateways/[serial]/devices/[device]/features/someprop') 18 | 19 | def test_setProperty_object(self): 20 | self.service.setProperty("someprop", "doaction", {'name': 'abc'}) 21 | self.oauth_mock.post.assert_called_once_with( 22 | '/features/installations/[id]/gateways/[serial]/devices/[device]/features/someprop/commands/doaction', '{"name": "abc"}') 23 | 24 | def test_setProperty_string(self): 25 | self.service.setProperty("someprop", "doaction", '{}') 26 | self.oauth_mock.post.assert_called_once_with( 27 | '/features/installations/[id]/gateways/[serial]/devices/[device]/features/someprop/commands/doaction', '{}') 28 | 29 | def test_getProperty_gateway(self): 30 | self.service = ViCareService(self.oauth_mock, self.accessor, ["type:gateway;VitoconnectOpto1"]) 31 | self.service.getProperty("someprop") 32 | self.oauth_mock.get.assert_called_once_with( 33 | '/features/installations/[id]/gateways/[serial]/features/someprop') 34 | 35 | def test_fetch_all_features_gateway(self): 36 | self.service = ViCareService(self.oauth_mock, self.accessor, ["type:gateway;VitoconnectOpto1"]) 37 | self.service.fetch_all_features() 38 | self.oauth_mock.get.assert_called_once_with( 39 | '/features/installations/[id]/gateways/[serial]/features/') 40 | -------------------------------------------------------------------------------- /tests/test_Solar.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatingDevice import HeatingDevice 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class SolarTest(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Solar.json') 10 | self.device = HeatingDevice(self.service) 11 | 12 | def test_isDomesticHotWaterDevice(self): 13 | self.assertEqual(self.device.isDomesticHotWaterDevice(), True) 14 | 15 | def test_isSolarThermalDevice(self): 16 | self.assertEqual(self.device.isSolarThermalDevice(), True) 17 | 18 | def test_isVentilationDevice(self): 19 | self.assertEqual(self.device.isVentilationDevice(), False) 20 | 21 | def test_getSolarStorageTemperature(self): 22 | self.assertEqual(self.device.getSolarStorageTemperature(), 41.5) 23 | 24 | def test_getSolarPowerProduction(self): 25 | self.assertEqual( 26 | self.device.getSolarPowerProduction(), [19.773, 20.642, 18.831, 22.672, 18.755, 14.513, 15.406, 13.115]) 27 | self.assertEqual( 28 | self.device.getSolarPowerProductionDays(), [19.773, 20.642, 18.831, 22.672, 18.755, 14.513, 15.406, 13.115]) 29 | self.assertEqual( 30 | self.device.getSolarPowerProductionToday(), 19.773) 31 | self.assertEqual( 32 | self.device.getSolarPowerProductionWeeks(), [19.773, 20.642, 18.831, 22.672, 18.755, 14.513, 15.406, 13.115]) 33 | self.assertEqual( 34 | self.device.getSolarPowerProductionThisWeek(), 19.773) 35 | self.assertEqual( 36 | self.device.getSolarPowerProductionMonths(), [19.773, 20.642, 18.831, 22.672, 18.755, 14.513, 15.406, 13.115]) 37 | self.assertEqual( 38 | self.device.getSolarPowerProductionThisMonth(), 19.773) 39 | self.assertEqual( 40 | self.device.getSolarPowerProductionYears(), [19.773, 20.642, 18.831, 22.672, 18.755, 14.513, 15.406, 13.115]) 41 | self.assertEqual( 42 | self.device.getSolarPowerProductionThisYear(), 19.773) 43 | 44 | def test_getSolarCollectorTemperature(self): 45 | self.assertEqual(self.device.getSolarCollectorTemperature(), 21.9) 46 | 47 | def test_getSolarPumpActive(self): 48 | self.assertEqual(self.device.getSolarPumpActive(), False) 49 | -------------------------------------------------------------------------------- /tests/test_Utils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import timedelta 3 | 4 | from PyViCare.PyViCareUtils import parse_time_as_delta 5 | 6 | 7 | class UtilTests(unittest.TestCase): 8 | 9 | def test_parse_timespan(self): 10 | self.assertEqual(timedelta(hours=2, minutes=4), parse_time_as_delta("02:04")) 11 | self.assertEqual(timedelta(hours=24, minutes=0), parse_time_as_delta("24:00")) 12 | -------------------------------------------------------------------------------- /tests/test_ViCareOAuthManager.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | 4 | from PyViCare.PyViCareOAuthManager import AbstractViCareOAuthManager 5 | from PyViCare.PyViCareUtils import (PyViCareCommandError, 6 | PyViCareInternalServerError, 7 | PyViCareRateLimitError) 8 | from tests.helper import readJson 9 | 10 | 11 | class OAuthManagerWithMock(AbstractViCareOAuthManager): 12 | def __init__(self, mock): 13 | super().__init__(mock) 14 | 15 | def renewToken(self): 16 | self.oauth_session.renewToken() 17 | 18 | 19 | class FakeResponse: 20 | def __init__(self, file_name): 21 | self.file_name = file_name 22 | 23 | def json(self): 24 | return readJson(self.file_name) 25 | 26 | 27 | class PyViCareServiceTest(unittest.TestCase): 28 | 29 | def setUp(self): 30 | self.oauth_mock = Mock() 31 | self.manager = OAuthManagerWithMock(self.oauth_mock) 32 | 33 | def test_get_raiseratelimit_ifthatreponse(self): 34 | self.oauth_mock.get.return_value = FakeResponse( 35 | 'response/errors/rate_limit.json') 36 | 37 | def func(): 38 | return self.manager.get("/") 39 | self.assertRaises(PyViCareRateLimitError, func) 40 | 41 | def test_post_raisecommanderror_ifthatreponse(self): 42 | self.oauth_mock.post.return_value = FakeResponse( 43 | 'response/errors/error_502.json') 44 | 45 | def func(): 46 | return self.manager.post("/", {}) 47 | self.assertRaises(PyViCareCommandError, func) 48 | 49 | def test_get_raiseservererror_ifthatreponse(self): 50 | self.oauth_mock.get.return_value = FakeResponse( 51 | 'response/errors/error_500.json') 52 | 53 | def func(): 54 | return self.manager.get("/") 55 | self.assertRaises(PyViCareInternalServerError, func) 56 | 57 | def test_get_renewtoken_ifexpired(self): 58 | self.oauth_mock.get.side_effect = [ 59 | FakeResponse('response/errors/expired_token.json'), # first call expired 60 | FakeResponse('response/Vitodens200W.json') # second call success 61 | ] 62 | self.manager.get("/") 63 | self.oauth_mock.renewToken.assert_called_once() 64 | 65 | def test_post_raiseratelimit_ifthatreponse(self): 66 | self.oauth_mock.post.return_value = FakeResponse( 67 | 'response/errors/rate_limit.json') 68 | 69 | def func(): 70 | return self.manager.post("/", "some") 71 | self.assertRaises(PyViCareRateLimitError, func) 72 | 73 | def test_post_renewtoken_ifexpired(self): 74 | self.oauth_mock.post.side_effect = [ 75 | FakeResponse('response/errors/expired_token.json'), # first call expired 76 | FakeResponse('response/Vitodens200W.json') # second call success 77 | ] 78 | self.manager.post("/", "some") 79 | self.oauth_mock.renewToken.assert_called_once() 80 | -------------------------------------------------------------------------------- /tests/test_VitoairFs300E.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareVentilationDevice import VentilationDevice 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class VitoairFs300(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/VitoairFs300E.json') 10 | self.device = VentilationDevice(self.service) 11 | 12 | def test_isDomesticHotWaterDevice(self): 13 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 14 | 15 | def test_isSolarThermalDevice(self): 16 | self.assertEqual(self.device.isSolarThermalDevice(), False) 17 | 18 | def test_isVentilationDevice(self): 19 | self.assertEqual(self.device.isVentilationDevice(), True) 20 | 21 | def test_getActiveMode(self): 22 | self.assertEqual(self.device.getActiveMode(), "sensorOverride") 23 | 24 | def test_getActiveProgram(self): 25 | self.assertEqual(self.device.getActiveProgram(), "levelFour") 26 | 27 | def test_getAvailableModes(self): 28 | expected_modes = ['permanent', 'ventilation', 'sensorOverride', 'sensorDriven'] 29 | self.assertListEqual(self.device.getAvailableModes(), expected_modes) 30 | 31 | def test_getAvailablePrograms(self): 32 | expected_programs = ['standby'] 33 | self.assertListEqual(self.device.getAvailablePrograms(), expected_programs) 34 | 35 | def test_getPermanentLevels(self): 36 | expected_levels = ['levelOne', 'levelTwo', 'levelThree', 'levelFour'] 37 | self.assertListEqual(expected_levels, self.device.getPermanentLevels()) 38 | 39 | def test_getSchedule(self): 40 | keys = ['active', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] 41 | self.assertListEqual(list(self.device.getSchedule().keys()), keys) 42 | 43 | def test_getSerial(self): 44 | self.assertEqual(self.device.getSerial(), "################") 45 | 46 | def test_getActiveVentilationMode(self): 47 | self.assertEqual("sensorOverride", self.device.getActiveVentilationMode()) 48 | 49 | def test_getVentilationModes(self): 50 | expected_modes = ['permanent', 'ventilation', 'sensorOverride', 'sensorDriven'] 51 | self.assertListEqual(expected_modes, self.device.getVentilationModes()) 52 | 53 | def test_getVentilationMode(self): 54 | self.assertEqual(False, self.device.getVentilationMode("filterChange")) 55 | 56 | def test_ventilationQuickmode(self): 57 | self.assertEqual(self.device.getVentilationQuickmode("forcedLevelFour"), False) 58 | self.assertEqual(self.device.getVentilationQuickmode("silent"), False) 59 | 60 | def test_ventilationQuickmodes(self): 61 | self.assertEqual(self.device.getVentilationQuickmodes(), [ 62 | "forcedLevelFour", 63 | "silent", 64 | ]) 65 | -------------------------------------------------------------------------------- /tests/test_Vitocal111S.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 4 | from PyViCare.PyViCareHeatPump import HeatPump 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitocal200(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitocal111S.json') 11 | self.device = HeatPump(self.service) 12 | 13 | def test_ventilation_state(self): 14 | self.assertEqual(self.device.getVentilationDemand(), "ventilation") 15 | self.assertEqual(self.device.getVentilationLevel(), "levelOne") 16 | self.assertEqual(self.device.getVentilationReason(), "schedule") 17 | 18 | def test_ventilationQuickmode(self): 19 | # quickmodes disabled 20 | with self.assertRaises(PyViCareNotSupportedFeatureError): 21 | self.device.getVentilationQuickmode("comfort") 22 | with self.assertRaises(PyViCareNotSupportedFeatureError): 23 | self.device.getVentilationQuickmode("eco") 24 | with self.assertRaises(PyViCareNotSupportedFeatureError): 25 | self.device.getVentilationQuickmode("holiday") 26 | 27 | def test_ventilationQuickmodes(self): 28 | self.assertEqual(self.device.getVentilationQuickmodes(), [ 29 | "comfort", 30 | "eco", 31 | "holiday", 32 | ]) 33 | -------------------------------------------------------------------------------- /tests/test_Vitocal151A.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatPump import HeatPump 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class Vitocal200(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Vitocal151A.json') 10 | self.device = HeatPump(self.service) 11 | 12 | def test_getPowerConsumptionCooling(self): 13 | self.assertEqual(self.device.getPowerConsumptionCoolingUnit(), "kilowattHour") 14 | self.assertEqual(self.device.getPowerConsumptionCoolingToday(), 0) 15 | self.assertEqual(self.device.getPowerConsumptionCoolingThisMonth(), 0.1) 16 | self.assertEqual(self.device.getPowerConsumptionCoolingThisYear(), 0.1) 17 | -------------------------------------------------------------------------------- /tests/test_Vitocal200.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatPump import HeatPump 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.helper import now_is 6 | from tests.ViCareServiceMock import ViCareServiceMock 7 | 8 | 9 | class Vitocal200(unittest.TestCase): 10 | def setUp(self): 11 | self.service = ViCareServiceMock('response/Vitocal200.json') 12 | self.device = HeatPump(self.service) 13 | 14 | def test_getCompressorActive(self): 15 | self.assertEqual(self.device.getCompressor(0).getActive(), False) 16 | 17 | def test_getCompressorHours(self): 18 | self.assertAlmostEqual( 19 | self.device.getCompressor(0).getHours(), 13651.9) 20 | 21 | def test_getAvailableCompressors(self): 22 | self.assertEqual(self.device.getAvailableCompressors(), ['0']) 23 | 24 | def test_getCompressorStarts(self): 25 | self.assertAlmostEqual( 26 | self.device.getCompressor(0).getStarts(), 6973) 27 | 28 | def test_getCompressorHoursLoadClass1(self): 29 | self.assertAlmostEqual( 30 | self.device.getCompressor(0).getHoursLoadClass1(), 366) 31 | 32 | def test_getCompressorHoursLoadClass2(self): 33 | self.assertAlmostEqual( 34 | self.device.getCompressor(0).getHoursLoadClass2(), 5579) 35 | 36 | def test_getCompressorHoursLoadClass3(self): 37 | self.assertAlmostEqual( 38 | self.device.getCompressor(0).getHoursLoadClass3(), 6024) 39 | 40 | def test_getCompressorHoursLoadClass4(self): 41 | self.assertAlmostEqual( 42 | self.device.getCompressor(0).getHoursLoadClass4(), 659) 43 | 44 | def test_getCompressorHoursLoadClass5(self): 45 | self.assertAlmostEqual( 46 | self.device.getCompressor(0).getHoursLoadClass5(), 715) 47 | 48 | def test_getCompressorPhase(self): 49 | self.assertEqual( 50 | self.device.getCompressor(0).getPhase(), "off") 51 | 52 | def test_getHeatingCurveSlope(self): 53 | self.assertAlmostEqual( 54 | self.device.getCircuit(0).getHeatingCurveSlope(), 0.4) 55 | 56 | def test_getHeatingCurveShift(self): 57 | self.assertAlmostEqual( 58 | self.device.getCircuit(0).getHeatingCurveShift(), -6) 59 | 60 | def test_getReturnTemperature(self): 61 | self.assertAlmostEqual(self.device.getReturnTemperature(), 22.7) 62 | 63 | def test_getReturnTemperaturePrimaryCircuit(self): 64 | self.assertRaises(PyViCareNotSupportedFeatureError, 65 | self.device.getReturnTemperaturePrimaryCircuit) 66 | 67 | def test_getSupplyTemperaturePrimaryCircuit(self): 68 | self.assertAlmostEqual( 69 | self.device.getSupplyTemperaturePrimaryCircuit(), 11.6) 70 | 71 | def test_getPrograms(self): 72 | expected_programs = ['comfort', 'eco', 'fixed', 'normal', 'reduced', 'standby'] 73 | self.assertListEqual( 74 | self.device.getCircuit(0).getPrograms(), expected_programs) 75 | 76 | def test_getModes(self): 77 | expected_modes = ['dhw', 'dhwAndHeatingCooling', 'standby'] 78 | self.assertListEqual( 79 | self.device.getCircuit(0).getModes(), expected_modes) 80 | 81 | def test_getDomesticHotWaterCirculationPumpActive(self): 82 | self.assertEqual( 83 | self.device.getDomesticHotWaterCirculationPumpActive(), False) 84 | 85 | def test_getDomesticHotWaterActiveMode_fri_10_10_time(self): 86 | with now_is('2021-09-10 10:10:00'): 87 | self.assertIsNone(self.device.getDomesticHotWaterActiveMode()) 88 | 89 | def test_getDomesticHotWaterDesiredTemperature_fri_10_10_time(self): 90 | with now_is('2021-09-10 10:10:00'): 91 | self.assertIsNone( 92 | self.device.getDomesticHotWaterDesiredTemperature()) 93 | 94 | def test_getDomesticHotWaterDesiredTemperature_fri_20_00_time(self): 95 | with now_is('2021-09-10 20:00:00'): 96 | self.assertEqual( 97 | self.device.getDomesticHotWaterDesiredTemperature(), 50) 98 | 99 | def test_getActiveProgramMinTemperature(self): 100 | self.assertEqual(self.device.getCircuit(0).getActiveProgramMinTemperature(), 10) 101 | 102 | def test_getActiveProgramMaxTemperature(self): 103 | self.assertEqual(self.device.getCircuit(0).getActiveProgramMaxTemperature(), 30) 104 | 105 | def test_getActiveProgramStepping(self): 106 | self.assertEqual(self.device.getCircuit(0).getActiveProgramStepping(), 1) 107 | 108 | def test_getNormalProgramMinTemperature(self): 109 | self.assertEqual(self.device.getCircuit(0).getProgramMinTemperature("normal"), 10) 110 | 111 | def test_getNormalProgramMaxTemperature(self): 112 | self.assertEqual(self.device.getCircuit(0).getProgramMaxTemperature("normal"), 30) 113 | 114 | def test_getNormalProgramStepping(self): 115 | self.assertEqual(self.device.getCircuit(0).getProgramStepping("normal"), 1) 116 | -------------------------------------------------------------------------------- /tests/test_Vitocal200S.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatPump import HeatPump 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class Vitocal200S(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Vitocal200S.json') 10 | self.device = HeatPump(self.service) 11 | 12 | def test_getDomesticHotWaterConfiguredTemperature(self): 13 | self.assertEqual( 14 | self.device.getDomesticHotWaterConfiguredTemperature(), 40) 15 | 16 | def test_getAvailableCompressors(self): 17 | self.assertEqual(self.device.getAvailableCompressors(), ['0']) 18 | 19 | def test_getDomesticHotWaterConfiguredTemperature2(self): 20 | self.assertEqual( 21 | self.device.getDomesticHotWaterConfiguredTemperature2(), 60) 22 | 23 | def test_getReturnTemperature(self): 24 | self.assertEqual( 25 | self.device.getReturnTemperature(), 27.9) 26 | 27 | def test_getSupplyTemperaturePrimaryCircuit(self): 28 | self.assertEqual( 29 | self.device.getSupplyTemperaturePrimaryCircuit(), 14.5) 30 | 31 | def test_getReturnTemperatureSecondaryCircuit(self): 32 | self.assertEqual( 33 | self.device.getReturnTemperatureSecondaryCircuit(), 27.9) 34 | -------------------------------------------------------------------------------- /tests/test_Vitocal222S.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatPump import HeatPump 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.helper import now_is 6 | from tests.ViCareServiceMock import ViCareServiceMock 7 | 8 | 9 | class Vitocal222S(unittest.TestCase): 10 | def setUp(self): 11 | self.service = ViCareServiceMock('response/Vitocal222S.json') 12 | self.device = HeatPump(self.service) 13 | 14 | def test_getDomesticHotWaterActiveMode_10_10_time(self): 15 | with now_is('2000-01-01 10:10:00'): 16 | self.assertEqual( 17 | self.device.getDomesticHotWaterActiveMode(), 'normal') 18 | 19 | def test_getCurrentDesiredTemperature(self): 20 | self.assertEqual( 21 | self.device.circuits[0].getCurrentDesiredTemperature(), 23) 22 | 23 | def test_isDomesticHotWaterDevice(self): 24 | self.assertEqual(self.device.isDomesticHotWaterDevice(), True) 25 | 26 | def test_isSolarThermalDevice(self): 27 | self.assertEqual(self.device.isSolarThermalDevice(), False) 28 | 29 | def test_isVentilationDevice(self): 30 | self.assertEqual(self.device.isVentilationDevice(), True) 31 | 32 | def test_getActiveVentilationMode(self): 33 | self.assertEqual("ventilation", self.device.getActiveVentilationMode()) 34 | 35 | def test_getVentilationModes(self): 36 | expected_modes = ['standby', 'standard', 'ventilation'] 37 | self.assertListEqual(expected_modes, self.device.getVentilationModes()) 38 | 39 | def test_getVentilationMode(self): 40 | self.assertEqual(False, self.device.getVentilationMode("standby")) 41 | 42 | def test_ventilationState(self): 43 | with self.assertRaises(PyViCareNotSupportedFeatureError): 44 | self.device.getVentilationDemand() 45 | with self.assertRaises(PyViCareNotSupportedFeatureError): 46 | self.device.getVentilationLevel() 47 | with self.assertRaises(PyViCareNotSupportedFeatureError): 48 | self.device.getVentilationReason() 49 | 50 | def test_ventilationQuickmode(self): 51 | with self.assertRaises(PyViCareNotSupportedFeatureError): 52 | self.device.getVentilationQuickmode("standby") 53 | 54 | def test_ventilationQuickmodes(self): 55 | self.assertEqual(self.device.getVentilationQuickmodes(), []) 56 | -------------------------------------------------------------------------------- /tests/test_Vitocal300G.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatPump import HeatPump 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class Vitocal300G(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Vitocal300G.json') 10 | self.device = HeatPump(self.service) 11 | 12 | def test_getCompressorActive(self): 13 | self.assertEqual(self.device.compressors[0].getActive(), False) 14 | 15 | def test_getCompressorHours(self): 16 | self.assertAlmostEqual( 17 | self.device.compressors[0].getHours(), 1762.41) 18 | 19 | def test_getCompressorStarts(self): 20 | self.assertAlmostEqual( 21 | self.device.compressors[0].getStarts(), 3012) 22 | 23 | def test_getCompressorHoursLoadClass1(self): 24 | self.assertAlmostEqual( 25 | self.device.compressors[0].getHoursLoadClass1(), 30) 26 | 27 | def test_getCompressorHoursLoadClass2(self): 28 | self.assertAlmostEqual( 29 | self.device.compressors[0].getHoursLoadClass2(), 703) 30 | 31 | def test_getCompressorHoursLoadClass3(self): 32 | self.assertAlmostEqual( 33 | self.device.compressors[0].getHoursLoadClass3(), 878) 34 | 35 | def test_getCompressorHoursLoadClass4(self): 36 | self.assertAlmostEqual( 37 | self.device.compressors[0].getHoursLoadClass4(), 117) 38 | 39 | def test_getCompressorHoursLoadClass5(self): 40 | self.assertAlmostEqual( 41 | self.device.compressors[0].getHoursLoadClass5(), 20) 42 | 43 | def test_getHeatingCurveSlope(self): 44 | self.assertAlmostEqual( 45 | self.device.circuits[0].getHeatingCurveSlope(), 0.8) 46 | 47 | def test_getHeatingCurveShift(self): 48 | self.assertAlmostEqual( 49 | self.device.circuits[0].getHeatingCurveShift(), -5) 50 | 51 | def test_getReturnTemperature(self): 52 | self.assertAlmostEqual(self.device.getReturnTemperature(), 18.9) 53 | 54 | def test_getReturnTemperaturePrimaryCircuit(self): 55 | self.assertAlmostEqual(self.device.getReturnTemperaturePrimaryCircuit(), 18.4) 56 | 57 | def test_getSupplyTemperaturePrimaryCircuit(self): 58 | self.assertAlmostEqual( 59 | self.device.getSupplyTemperaturePrimaryCircuit(), 18.2) 60 | 61 | def test_getPrograms(self): 62 | expected_programs = ['comfort', 'eco', 'fixed', 'holiday', 'normal', 'reduced', 'standby'] 63 | self.assertListEqual( 64 | self.device.circuits[0].getPrograms(), expected_programs) 65 | 66 | def test_getModes(self): 67 | expected_modes = ['dhw', 'dhwAndHeating', 'forcedNormal', 'forcedReduced', 'standby', 'normalStandby'] 68 | self.assertListEqual( 69 | self.device.circuits[0].getModes(), expected_modes) 70 | 71 | def test_getDomesticHotWaterCirculationPumpActive(self): 72 | self.assertEqual( 73 | self.device.getDomesticHotWaterCirculationPumpActive(), False) 74 | -------------------------------------------------------------------------------- /tests/test_Vitocal333G.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatPump import HeatPump 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitocal300G(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitocal333G.json') 11 | self.device = HeatPump(self.service) 12 | 13 | def test_getDomesticHotWaterStorageTemperature(self): 14 | self.assertEqual( 15 | self.device.getDomesticHotWaterStorageTemperature(), 47.5) 16 | 17 | def test_getHotWaterStorageTemperatureTop(self): 18 | self.assertEqual( 19 | self.device.getHotWaterStorageTemperatureTop(), 47.5) 20 | 21 | def test_getActiveVentilationMode(self): 22 | self.assertEqual("ventilation", self.device.getActiveVentilationMode()) 23 | 24 | def test_getVentilationModes(self): 25 | expected_modes = ['standby', 'standard', 'ventilation'] 26 | self.assertListEqual(expected_modes, self.device.getVentilationModes()) 27 | 28 | def test_getVentilationMode(self): 29 | self.assertEqual(False, self.device.getVentilationMode("standby")) 30 | 31 | def test_ventilationState(self): 32 | with self.assertRaises(PyViCareNotSupportedFeatureError): 33 | self.device.getVentilationDemand() 34 | with self.assertRaises(PyViCareNotSupportedFeatureError): 35 | self.device.getVentilationLevel() 36 | with self.assertRaises(PyViCareNotSupportedFeatureError): 37 | self.device.getVentilationReason() 38 | 39 | def test_ventilationQuickmode(self): 40 | with self.assertRaises(PyViCareNotSupportedFeatureError): 41 | self.device.getVentilationQuickmode("standby") 42 | 43 | def test_ventilationQuickmodes(self): 44 | self.assertEqual(self.device.getVentilationQuickmodes(), []) 45 | -------------------------------------------------------------------------------- /tests/test_Vitocaldens222F.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHybrid import Hybrid 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class Vitocaldens222F(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Vitocaldens222F.json') 10 | self.device = Hybrid(self.service) 11 | 12 | def test_isDomesticHotWaterDevice(self): 13 | self.assertEqual(self.device.isDomesticHotWaterDevice(), True) 14 | 15 | def test_isSolarThermalDevice(self): 16 | self.assertEqual(self.device.isSolarThermalDevice(), False) 17 | 18 | def test_isVentilationDevice(self): 19 | self.assertEqual(self.device.isVentilationDevice(), False) 20 | 21 | def test_getAvailableCircuits(self): 22 | self.assertEqual(self.device.getAvailableCircuits(), ['1']) 23 | 24 | def test_getAvailableBurners(self): 25 | self.assertEqual(self.device.getAvailableBurners(), ['0']) 26 | 27 | def test_getAvailableCompressors(self): 28 | self.assertEqual(self.device.getAvailableCompressors(), ['0']) 29 | 30 | def test_getActive(self): 31 | self.assertEqual(self.device.burners[0].getActive(), False) 32 | 33 | @unittest.skip("dump is not up to date, underlying data point was rernamed") 34 | def test_getBufferTopTemperature(self): 35 | self.assertEqual( 36 | self.device.getBufferTopTemperature(), 36) 37 | 38 | @unittest.skip("dump is not up to date, underlying data point was rernamed") 39 | def test_getBufferMainTemperature(self): 40 | self.assertEqual( 41 | self.device.getBufferMainTemperature(), 36) 42 | 43 | def test_getBurnerStarts(self): 44 | self.assertEqual(self.device.getBurner(0).getStarts(), 1306) 45 | 46 | def test_getBurnerHours(self): 47 | self.assertEqual(self.device.getBurner(0).getHours(), 1639) 48 | 49 | def test_getBurnerModulation(self): 50 | self.assertEqual(self.device.getBurner(0).getModulation(), 0) 51 | 52 | def test_getCompressorHours(self): 53 | self.assertEqual(self.device.getCompressor(0).getHours(), 1.4) 54 | 55 | def test_getPrograms(self): 56 | expected_programs = ['comfort', 'eco', 'fixed', 'normal', 'reduced', 'standby'] 57 | self.assertListEqual( 58 | self.device.getCircuit(1).getPrograms(), expected_programs) 59 | 60 | def test_getModes(self): 61 | expected_modes = ['standby', 'dhw', 'dhwAndHeating'] 62 | self.assertListEqual( 63 | self.device.getCircuit(1).getModes(), expected_modes) 64 | 65 | def test_getFrostProtectionActive(self): 66 | self.assertEqual( 67 | self.device.getCircuit(1).getFrostProtectionActive(), False) 68 | 69 | def test_getDomesticHotWaterCirculationPumpActive(self): 70 | self.assertEqual( 71 | self.device.getDomesticHotWaterCirculationPumpActive(), False) 72 | 73 | def test_getDomesticHotWaterOutletTemperature(self): 74 | self.assertEqual( 75 | self.device.getDomesticHotWaterOutletTemperature(), 41.7) 76 | 77 | def test_getDomesticHotWaterCirculationScheduleModes(self): 78 | self.assertEqual( 79 | self.device.getDomesticHotWaterCirculationScheduleModes(), ['5/25-cycles', '5/10-cycles', 'on']) 80 | 81 | def test_getOutsideTemperature(self): 82 | self.assertEqual( 83 | self.device.getOutsideTemperature(), 15.3) 84 | 85 | @unittest.skip("dump is not up to date, underlying data point was rernamed") 86 | def test_getHotWaterStorageTemperatureTop(self): 87 | self.assertEqual( 88 | self.device.getHotWaterStorageTemperatureTop(), 50.9) 89 | -------------------------------------------------------------------------------- /tests/test_Vitocharge05.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareElectricalEnergySystem import ElectricalEnergySystem 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class Vitocharge05(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Vitocharge05.json') 10 | self.device = ElectricalEnergySystem(self.service) 11 | 12 | def test_isDomesticHotWaterDevice(self): 13 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 14 | 15 | def test_isSolarThermalDevice(self): 16 | self.assertEqual(self.device.isSolarThermalDevice(), False) 17 | 18 | def test_isVentilationDevice(self): 19 | self.assertEqual(self.device.isVentilationDevice(), False) 20 | 21 | def test_getSerial(self): 22 | self.assertEqual(self.device.getSerial(), '################') 23 | 24 | def test_getPointOfCommonCouplingTransferPowerExchange(self): 25 | self.assertEqual(self.device.getPointOfCommonCouplingTransferPowerExchange(), 6624) 26 | 27 | def test_getPhotovoltaicProductionCumulatedUnit(self): 28 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedUnit(), "wattHour") 29 | 30 | def test_getPhotovoltaicProductionCumulatedCurrentDay(self): 31 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentDay(), 4534) 32 | 33 | def test_getPhotovoltaicProductionCumulatedCurrentWeek(self): 34 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentWeek(), 12483) 35 | 36 | def test_getPhotovoltaicProductionCumulatedCurrentMonth(self): 37 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentMonth(), 23498) 38 | 39 | def test_getPhotovoltaicProductionCumulatedCurrentYear(self): 40 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentYear(), 23498) 41 | 42 | def test_getPhotovoltaicProductionCumulatedLifeCycle(self): 43 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedLifeCycle(), 23498) 44 | 45 | def test_getPhotovoltaicStatus(self): 46 | self.assertEqual(self.device.getPhotovoltaicStatus(), "ready") 47 | 48 | def test_getPhotovoltaicProductionCurrent(self): 49 | self.assertEqual(self.device.getPhotovoltaicProductionCurrent(), 0) 50 | 51 | def test_getPhotovoltaicProductionCurrentUnit(self): 52 | self.assertEqual(self.device.getPhotovoltaicProductionCurrentUnit(), "kilowatt") 53 | 54 | def test_getPointOfCommonCouplingTransferConsumptionTotal(self): 55 | self.assertEqual(self.device.getPointOfCommonCouplingTransferConsumptionTotal(), 3258900) 56 | 57 | def test_getPointOfCommonCouplingTransferConsumptionTotalUnit(self): 58 | self.assertEqual(self.device.getPointOfCommonCouplingTransferConsumptionTotalUnit(), "wattHour") 59 | 60 | def test_getPointOfCommonCouplingTransferFeedInTotal(self): 61 | self.assertEqual(self.device.getPointOfCommonCouplingTransferFeedInTotal(), 29200) 62 | 63 | def test_getPointOfCommonCouplingTransferFeedInTotalUnit(self): 64 | self.assertEqual(self.device.getPointOfCommonCouplingTransferFeedInTotalUnit(), "wattHour") 65 | 66 | def test_getElectricalEnergySystemTransferDischargeCumulatedUnit(self): 67 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedUnit(), "wattHour") 68 | 69 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentDay(self): 70 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentDay(), 1245) 71 | 72 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentWeek(self): 73 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentWeek(), 3232) 74 | 75 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentMonth(self): 76 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentMonth(), 3982) 77 | 78 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentYear(self): 79 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentYear(), 3982) 80 | 81 | def test_getElectricalEnergySystemTransferDischargeCumulatedLifeCycle(self): 82 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedLifeCycle(), 3982) 83 | 84 | def test_getElectricalEnergySystemSOC(self): 85 | self.assertEqual(self.device.getElectricalEnergySystemSOC(), 0) 86 | 87 | def test_getElectricalEnergySystemSOCUnit(self): 88 | self.assertEqual(self.device.getElectricalEnergySystemSOCUnit(), "percent") 89 | 90 | def test_getElectricalEnergySystemPower(self): 91 | self.assertEqual(self.device.getElectricalEnergySystemPower(), 0) 92 | 93 | def test_getElectricalEnergySystemPowerUnit(self): 94 | self.assertEqual(self.device.getElectricalEnergySystemPowerUnit(), "watt") 95 | 96 | def test_getElectricalEnergySystemOperationState(self): 97 | self.assertEqual(self.device.getElectricalEnergySystemOperationState(), "standby") 98 | -------------------------------------------------------------------------------- /tests/test_VitochargeVX3.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareElectricalEnergySystem import ElectricalEnergySystem 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class VitochargeVX3(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/VitochargeVX3.json') 10 | self.device = ElectricalEnergySystem(self.service) 11 | 12 | def test_isDomesticHotWaterDevice(self): 13 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 14 | 15 | def test_isSolarThermalDevice(self): 16 | self.assertEqual(self.device.isSolarThermalDevice(), False) 17 | 18 | def test_isVentilationDevice(self): 19 | self.assertEqual(self.device.isVentilationDevice(), False) 20 | 21 | def test_getSerial(self): 22 | self.assertEqual(self.device.getSerial(), '################') 23 | 24 | def test_getPointOfCommonCouplingTransferPowerExchange(self): 25 | self.assertEqual(self.device.getPointOfCommonCouplingTransferPowerExchange(), 0) 26 | 27 | def test_getPhotovoltaicProductionCumulatedUnit(self): 28 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedUnit(), "wattHour") 29 | 30 | def test_getPhotovoltaicProductionCumulatedCurrentDay(self): 31 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentDay(), 47440) 32 | 33 | def test_getPhotovoltaicProductionCumulatedCurrentWeek(self): 34 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentWeek(), 208436) 35 | 36 | def test_getPhotovoltaicProductionCumulatedCurrentMonth(self): 37 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentMonth(), 487670) 38 | 39 | def test_getPhotovoltaicProductionCumulatedCurrentYear(self): 40 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedCurrentYear(), 487670) 41 | 42 | def test_getPhotovoltaicProductionCumulatedLifeCycle(self): 43 | self.assertEqual(self.device.getPhotovoltaicProductionCumulatedLifeCycle(), 487670) 44 | 45 | def test_getPhotovoltaicStatus(self): 46 | self.assertEqual(self.device.getPhotovoltaicStatus(), "ready") 47 | 48 | def test_getPhotovoltaicProductionCurrent(self): 49 | self.assertEqual(self.device.getPhotovoltaicProductionCurrent(), 0) 50 | 51 | def test_getPhotovoltaicProductionCurrentUnit(self): 52 | self.assertEqual(self.device.getPhotovoltaicProductionCurrentUnit(), "kilowatt") 53 | 54 | def test_getPointOfCommonCouplingTransferConsumptionTotal(self): 55 | self.assertEqual(self.device.getPointOfCommonCouplingTransferConsumptionTotal(), 7700) 56 | 57 | def test_getPointOfCommonCouplingTransferConsumptionTotalUnit(self): 58 | self.assertEqual(self.device.getPointOfCommonCouplingTransferConsumptionTotalUnit(), "wattHour") 59 | 60 | def test_getPointOfCommonCouplingTransferFeedInTotal(self): 61 | self.assertEqual(self.device.getPointOfCommonCouplingTransferFeedInTotal(), 298900) 62 | 63 | def test_getPointOfCommonCouplingTransferFeedInTotalUnit(self): 64 | self.assertEqual(self.device.getPointOfCommonCouplingTransferFeedInTotalUnit(), "wattHour") 65 | 66 | def test_getElectricalEnergySystemTransferDischargeCumulatedUnit(self): 67 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedUnit(), "wattHour") 68 | 69 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentDay(self): 70 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentDay(), 4751) 71 | 72 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentWeek(self): 73 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentWeek(), 29820) 74 | 75 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentMonth(self): 76 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentMonth(), 66926) 77 | 78 | def test_getElectricalEnergySystemTransferDischargeCumulatedCurrentYear(self): 79 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedCurrentYear(), 66926) 80 | 81 | def test_getElectricalEnergySystemTransferDischargeCumulatedLifeCycle(self): 82 | self.assertEqual(self.device.getElectricalEnergySystemTransferDischargeCumulatedLifeCycle(), 66926) 83 | 84 | def test_getElectricalEnergySystemSOC(self): 85 | self.assertEqual(self.device.getElectricalEnergySystemSOC(), 91) 86 | 87 | def test_getElectricalEnergySystemSOCUnit(self): 88 | self.assertEqual(self.device.getElectricalEnergySystemSOCUnit(), "percent") 89 | 90 | def test_getElectricalEnergySystemPower(self): 91 | self.assertEqual(self.device.getElectricalEnergySystemPower(), 700) 92 | 93 | def test_getElectricalEnergySystemPowerUnit(self): 94 | self.assertEqual(self.device.getElectricalEnergySystemPowerUnit(), "watt") 95 | 96 | def test_getElectricalEnergySystemOperationState(self): 97 | self.assertEqual(self.device.getElectricalEnergySystemOperationState(), "discharge") 98 | -------------------------------------------------------------------------------- /tests/test_VitoconnectOpto1.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGateway import Gateway 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class VitoconnectOpto1(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/VitoconnectOpto1.json') 10 | self.device = Gateway(self.service) 11 | 12 | def test_isDomesticHotWaterDevice(self): 13 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 14 | 15 | def test_isSolarThermalDevice(self): 16 | self.assertEqual(self.device.isSolarThermalDevice(), False) 17 | 18 | def test_isVentilationDevice(self): 19 | self.assertEqual(self.device.isVentilationDevice(), False) 20 | 21 | def test_getSerial(self): 22 | self.assertEqual( 23 | self.device.getSerial(), "################") 24 | 25 | def test_getWifiSignalStrength(self): 26 | self.assertEqual( 27 | self.device.getWifiSignalStrength(), -69) 28 | -------------------------------------------------------------------------------- /tests/test_VitoconnectOpto2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGateway import Gateway 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class VitoconnectOpto2(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/VitoconnectOpto2.json') 10 | self.device = Gateway(self.service) 11 | 12 | def test_getSerial(self): 13 | self.assertEqual( 14 | self.device.getSerial(), "##############") 15 | 16 | def test_getWifiSignalStrength(self): 17 | self.assertEqual( 18 | self.device.getWifiSignalStrength(), -41) 19 | -------------------------------------------------------------------------------- /tests/test_Vitodens100W.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGazBoiler import GazBoiler 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class Vitodens100W(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Vitodens100W.json') 10 | self.device = GazBoiler(self.service) 11 | 12 | def test_getActive(self): 13 | self.assertEqual(self.device.burners[0].getActive(), False) 14 | 15 | def test_getBurnerStarts(self): 16 | self.assertEqual(self.device.burners[0].getStarts(), 6826) 17 | 18 | def test_getBurnerHours(self): 19 | self.assertEqual(self.device.burners[0].getHours(), 675) 20 | 21 | def test_getBurnerModulation(self): 22 | self.assertEqual(self.device.burners[0].getModulation(), 0) 23 | 24 | def test_getGasSummaryConsumptionHeatingCurrentDay(self): 25 | self.assertEqual(self.device.getGasSummaryConsumptionHeatingCurrentDay(), 11.2) 26 | self.assertEqual(self.device.getGasSummaryConsumptionHeatingUnit(), "cubicMeter") 27 | 28 | def test_getGasSummaryConsumptionDomesticHotWaterCurrentMonth(self): 29 | self.assertEqual(self.device.getGasSummaryConsumptionDomesticHotWaterCurrentMonth(), 13.7) 30 | self.assertEqual(self.device.getGasSummaryConsumptionDomesticHotWaterUnit(), "cubicMeter") 31 | 32 | def test_getPowerSummaryConsumptionHeatingCurrentDay(self): 33 | self.assertEqual(self.device.getPowerSummaryConsumptionHeatingCurrentDay(), 0.9) 34 | self.assertEqual(self.device.getPowerSummaryConsumptionHeatingUnit(), "kilowattHour") 35 | 36 | def test_getPowerSummaryConsumptionDomesticHotWaterCurrentYear(self): 37 | self.assertEqual(self.device.getPowerSummaryConsumptionDomesticHotWaterCurrentYear(), 18) 38 | self.assertEqual(self.device.getPowerSummaryConsumptionDomesticHotWaterUnit(), "kilowattHour") 39 | 40 | def test_getGasSummaryConsumptionDomesticHotWaterUnit(self): 41 | self.assertEqual(self.device.getGasSummaryConsumptionDomesticHotWaterUnit(), "cubicMeter") 42 | -------------------------------------------------------------------------------- /tests/test_Vitodens200W.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGazBoiler import GazBoiler 4 | from tests.helper import now_is 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitodens200W(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitodens200W.json') 11 | self.device = GazBoiler(self.service) 12 | 13 | def test_isDomesticHotWaterDevice(self): 14 | self.assertEqual(self.device.isDomesticHotWaterDevice(), True) 15 | 16 | def test_isSolarThermalDevice(self): 17 | self.assertEqual(self.device.isSolarThermalDevice(), False) 18 | 19 | def test_isVentilationDevice(self): 20 | self.assertEqual(self.device.isVentilationDevice(), False) 21 | 22 | def test_getSerial(self): 23 | self.assertEqual(self.device.getSerial(), '################') 24 | 25 | def test_getBoilerCommonSupplyTemperature(self): 26 | self.assertEqual(self.device.getBoilerCommonSupplyTemperature(), 44.4) 27 | 28 | def test_getActive(self): 29 | self.assertEqual(self.device.burners[0].getActive(), False) 30 | 31 | def test_getDomesticHotWaterActive(self): 32 | self.assertEqual(self.device.getDomesticHotWaterActive(), True) 33 | 34 | def test_getBurnerStarts(self): 35 | self.assertEqual(self.device.burners[0].getStarts(), 8237) 36 | 37 | def test_getBurnerHours(self): 38 | self.assertEqual(self.device.burners[0].getHours(), 5644) 39 | 40 | def test_getBurnerModulation(self): 41 | self.assertEqual(self.device.burners[0].getModulation(), 0) 42 | 43 | def test_getPrograms(self): 44 | expected_programs = ['comfort', 'forcedLastFromSchedule', 'normal', 'reduced', 'standby'] 45 | self.assertListEqual( 46 | self.device.circuits[0].getPrograms(), expected_programs) 47 | 48 | def test_getModes(self): 49 | expected_modes = ['standby', 'heating', 'dhw', 'dhwAndHeating'] 50 | self.assertListEqual( 51 | self.device.circuits[0].getModes(), expected_modes) 52 | 53 | def test_getPowerConsumptionDays(self): 54 | expected_days = [0.1, 0.2, 0.2, 0.2, 0.2, 0.4, 0.4, 0.1] 55 | self.assertEqual(self.device.getPowerConsumptionDays(), expected_days) 56 | 57 | def test_getDomesticHotWaterMaxTemperature(self): 58 | self.assertEqual(self.device.getDomesticHotWaterMaxTemperature(), 60) 59 | 60 | def test_getDomesticHotWaterMinTemperature(self): 61 | self.assertEqual(self.device.getDomesticHotWaterMinTemperature(), 10) 62 | 63 | def test_getFrostProtectionActive(self): 64 | self.assertEqual( 65 | self.device.circuits[0].getFrostProtectionActive(), False) 66 | 67 | def test_getDomesticHotWaterCirculationPumpActive(self): 68 | self.assertEqual( 69 | self.device.getDomesticHotWaterCirculationPumpActive(), False) 70 | 71 | def test_getDomesticHotWaterOutletTemperature(self): 72 | self.assertEqual( 73 | self.device.getDomesticHotWaterOutletTemperature(), 39.1) 74 | 75 | def test_getDomesticHotWaterConfiguredTemperature(self): 76 | self.assertEqual( 77 | self.device.getDomesticHotWaterConfiguredTemperature(), 55) 78 | 79 | def test_getDomesticHotWaterCirculationScheduleModes(self): 80 | self.assertEqual( 81 | self.device.getDomesticHotWaterCirculationScheduleModes(), ['on']) 82 | 83 | def test_getDomesticHotWaterCirculationMode_wed_07_30_time(self): 84 | with now_is('2021-09-08 07:30:00'): 85 | self.assertEqual( 86 | self.device.getDomesticHotWaterCirculationMode(), 'on') 87 | 88 | def test_getDomesticHotWaterCirculationMode_wed_10_10_time(self): 89 | with now_is('2021-09-08 10:10:00'): 90 | self.assertEqual( 91 | self.device.getDomesticHotWaterCirculationMode(), 'off') 92 | 93 | def test_getGasConsumptionHeatingToday(self): 94 | self.assertEqual( 95 | self.device.getGasConsumptionHeatingToday(), 0) 96 | 97 | def test_getGasConsumptionDomesticHotWaterToday(self): 98 | self.assertEqual( 99 | self.device.getGasConsumptionDomesticHotWaterToday(), 1.3) 100 | 101 | def test_getPowerConsumptionToday(self): 102 | self.assertEqual( 103 | self.device.getPowerConsumptionToday(), 0.1) 104 | -------------------------------------------------------------------------------- /tests/test_Vitodens200W_2.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGazBoiler import GazBoiler 4 | from tests.helper import now_is 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitodens200W_2(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitodens200W_2.json') 11 | self.device = GazBoiler(self.service) 12 | 13 | def test_getSerial(self): 14 | self.assertEqual(self.device.getSerial(), '################') 15 | 16 | def test_getActive(self): 17 | self.assertEqual(self.device.burners[0].getActive(), False) 18 | 19 | def test_getDomesticHotWaterActive(self): 20 | self.assertEqual(self.device.getDomesticHotWaterActive(), True) 21 | 22 | def test_getBurnerStarts(self): 23 | self.assertEqual(self.device.burners[0].getStarts(), 41460) 24 | 25 | def test_getBurnerHours(self): 26 | self.assertEqual(self.device.burners[0].getHours(), 19016.7) 27 | 28 | def test_getBurnerModulation(self): 29 | self.assertEqual(self.device.burners[0].getModulation(), 0) 30 | 31 | def test_getPrograms(self): 32 | expected_programs = ['comfort', 'eco', 'external', 'holiday', 'normal', 'reduced', 'standby'] 33 | self.assertListEqual( 34 | self.device.circuits[0].getPrograms(), expected_programs) 35 | 36 | def test_getModes(self): 37 | expected_modes = ['standby', 'dhw', 'dhwAndHeating', 'forcedReduced', 'forcedNormal'] 38 | self.assertListEqual( 39 | self.device.circuits[0].getModes(), expected_modes) 40 | 41 | def test_getPowerConsumptionDays(self): 42 | expected_days = [0.283, 0.269, 0.272, 0.279, 0.287, 0.271, 0.273, 0.269] 43 | self.assertEqual(self.device.getPowerConsumptionDays(), expected_days) 44 | 45 | def test_getDomesticHotWaterMaxTemperature(self): 46 | self.assertEqual(self.device.getDomesticHotWaterMaxTemperature(), 60) 47 | 48 | def test_getDomesticHotWaterMinTemperature(self): 49 | self.assertEqual(self.device.getDomesticHotWaterMinTemperature(), 10) 50 | 51 | def test_getFrostProtectionActive(self): 52 | self.assertEqual( 53 | self.device.circuits[0].getFrostProtectionActive(), False) 54 | 55 | def test_getDomesticHotWaterCirculationPumpActive(self): 56 | self.assertEqual( 57 | self.device.getDomesticHotWaterCirculationPumpActive(), True) 58 | 59 | def test_getDomesticHotWaterConfiguredTemperature(self): 60 | self.assertEqual( 61 | self.device.getDomesticHotWaterConfiguredTemperature(), 55) 62 | 63 | def test_getDomesticHotWaterCirculationScheduleModes(self): 64 | self.assertEqual( 65 | self.device.getDomesticHotWaterCirculationScheduleModes(), ['on']) 66 | 67 | def test_getDomesticHotWaterCirculationMode_wed_07_30_time(self): 68 | with now_is('2021-09-08 07:30:00'): 69 | self.assertEqual( 70 | self.device.getDomesticHotWaterCirculationMode(), 'on') 71 | 72 | def test_getDomesticHotWaterCirculationMode_wed_10_10_time(self): 73 | with now_is('2021-09-08 10:10:00'): 74 | self.assertEqual( 75 | self.device.getDomesticHotWaterCirculationMode(), 'on') 76 | 77 | def test_getGasConsumptionHeatingUnit(self): 78 | self.assertEqual( 79 | self.device.getGasConsumptionHeatingUnit(), "kilowattHour") 80 | 81 | def test_getGasConsumptionHeatingToday(self): 82 | self.assertEqual( 83 | self.device.getGasConsumptionHeatingToday(), 0) 84 | 85 | def test_getGasConsumptionDomesticHotWaterUnit(self): 86 | self.assertEqual( 87 | self.device.getGasConsumptionDomesticHotWaterUnit(), "kilowattHour") 88 | 89 | def test_getGasConsumptionDomesticHotWaterToday(self): 90 | self.assertEqual( 91 | self.device.getGasConsumptionDomesticHotWaterToday(), 29) 92 | 93 | def test_getPowerConsumptionUnit(self): 94 | self.assertEqual( 95 | self.device.getPowerConsumptionUnit(), "kilowattHour") 96 | 97 | def test_getPowerConsumptionToday(self): 98 | self.assertEqual( 99 | self.device.getPowerConsumptionToday(), 0.283) 100 | -------------------------------------------------------------------------------- /tests/test_Vitodens222W.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGazBoiler import GazBoiler 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitodens222W(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitodens222W.json') 11 | self.device = GazBoiler(self.service) 12 | 13 | def test_getActive(self): 14 | self.assertEqual(self.device.burners[0].getActive(), True) 15 | 16 | def test_getBurnerStarts(self): 17 | self.assertEqual(self.device.burners[0].getStarts(), 8299) 18 | 19 | def test_getBurnerHours(self): 20 | self.assertEqual(self.device.burners[0].getHours(), 5674) 21 | 22 | def test_getBurnerModulation(self): 23 | self.assertEqual(self.device.burners[0].getModulation(), 15.8) 24 | 25 | def test_getPrograms(self): 26 | expected_programs = ['comfort', 'forcedLastFromSchedule', 'normal', 'reduced', 'standby'] 27 | self.assertListEqual( 28 | self.device.circuits[0].getPrograms(), expected_programs) 29 | 30 | def test_getModes(self): 31 | expected_modes = ['standby', 'heating', 'dhw', 'dhwAndHeating'] 32 | self.assertListEqual( 33 | self.device.circuits[0].getModes(), expected_modes) 34 | 35 | def test_getPowerConsumptionDays(self): 36 | expected_consumption = [0.4, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1] 37 | self.assertListEqual(self.device.getPowerConsumptionDays(), expected_consumption) 38 | 39 | def test_getFrostProtectionActive(self): 40 | self.assertEqual( 41 | self.device.circuits[0].getFrostProtectionActive(), False) 42 | 43 | def test_getDomesticHotWaterCirculationPumpActive(self): 44 | self.assertEqual( 45 | self.device.getDomesticHotWaterCirculationPumpActive(), False) 46 | 47 | def test_getDomesticHotWaterOutletTemperature(self): 48 | self.assertEqual( 49 | self.device.getDomesticHotWaterOutletTemperature(), 39.8) 50 | 51 | def test_getDomesticHotWaterCirculationScheduleModes(self): 52 | self.assertEqual( 53 | self.device.getDomesticHotWaterCirculationScheduleModes(), ['on']) 54 | 55 | def test_getOutsideTemperature(self): 56 | self.assertEqual( 57 | self.device.getOutsideTemperature(), 11.9) 58 | 59 | def test_getOneTimeCharge(self): 60 | self.assertEqual( 61 | self.device.getOneTimeCharge(), False) 62 | 63 | def test_getBoilerTemperature(self): 64 | self.assertRaises(PyViCareNotSupportedFeatureError, self.device.getBoilerTemperature) 65 | -------------------------------------------------------------------------------- /tests/test_Vitodens300W.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGazBoiler import GazBoiler 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitodens300W(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitodens300W.json') 11 | self.device = GazBoiler(self.service) 12 | 13 | def test_getActive(self): 14 | self.assertEqual(self.device.burners[0].getActive(), False) 15 | 16 | def test_getDomesticHotWaterChargingLevel(self): 17 | self.assertEqual(self.device.getDomesticHotWaterChargingLevel(), 0) 18 | 19 | def test_getBurnerStarts(self): 20 | self.assertEqual(self.device.burners[0].getStarts(), 14315) 21 | 22 | def test_getBurnerHours(self): 23 | self.assertEqual(self.device.burners[0].getHours(), 18726.3) 24 | 25 | def test_getBurnerModulation(self): 26 | self.assertEqual(self.device.burners[0].getModulation(), 0) 27 | 28 | def test_getPrograms(self): 29 | expected_programs = ['comfort', 'eco', 'external', 'holiday', 'normal', 'reduced', 'standby'] 30 | self.assertListEqual( 31 | self.device.circuits[0].getPrograms(), expected_programs) 32 | 33 | def test_getModes(self): 34 | expected_modes = ['standby', 'dhw', 'dhwAndHeating', 'forcedReduced', 'forcedNormal'] 35 | self.assertListEqual( 36 | self.device.circuits[0].getModes(), expected_modes) 37 | 38 | def test_getPowerConsumptionDays(self): 39 | expected_consumption = [0.219, 0.316, 0.32, 0.325, 0.311, 0.317, 0.312, 0.313] 40 | self.assertEqual(self.device.getPowerConsumptionDays(), 41 | expected_consumption) 42 | 43 | def test_getFrostProtectionActive(self): 44 | self.assertEqual( 45 | self.device.circuits[0].getFrostProtectionActive(), False) 46 | 47 | def test_getDomesticHotWaterCirculationPumpActive(self): 48 | self.assertEqual( 49 | self.device.getDomesticHotWaterCirculationPumpActive(), True) 50 | 51 | def test_getCurrentDesiredTemperature(self): 52 | self.assertEqual( 53 | self.device.circuits[0].getCurrentDesiredTemperature(), None) 54 | 55 | # Is currently (August, 2021) not supported by the Viessman API even though it works for the Vitodens 200W. 56 | def test_getDomesticHotWaterOutletTemperature(self): 57 | self.assertRaises(PyViCareNotSupportedFeatureError, 58 | self.device.getDomesticHotWaterOutletTemperature) 59 | 60 | def test_getDomesticHotWaterCirculationScheduleModes(self): 61 | self.assertRaises(PyViCareNotSupportedFeatureError, 62 | self.device.getDomesticHotWaterCirculationScheduleModes) 63 | -------------------------------------------------------------------------------- /tests/test_Vitodens333F.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareGazBoiler import GazBoiler 4 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitodens333F(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitodens333F.json') 11 | self.device = GazBoiler(self.service) 12 | 13 | # currently missing an up-to-date test response 14 | def test_getActive(self): 15 | self.assertRaises(PyViCareNotSupportedFeatureError, self.device.burners[0].getActive) 16 | 17 | def test_getBurnerStarts(self): 18 | self.assertEqual(self.device.burners[0].getStarts(), 13987) 19 | 20 | def test_getBurnerHours(self): 21 | self.assertEqual(self.device.burners[0].getHours(), 14071.8) 22 | 23 | def test_getBurnerModulation(self): 24 | self.assertEqual(self.device.burners[0].getModulation(), 0) 25 | 26 | def test_getPrograms(self): 27 | expected_programs = ['comfort', 'eco', 'external', 'holiday', 'normal', 'reduced', 'standby'] 28 | self.assertListEqual( 29 | self.device.circuits[0].getPrograms(), expected_programs) 30 | 31 | def test_getModes(self): 32 | expected_modes = ['standby', 'dhw', 'dhwAndHeating', 33 | 'forcedReduced', 'forcedNormal'] 34 | self.assertListEqual( 35 | self.device.circuits[0].getModes(), expected_modes) 36 | 37 | # the api has changed, and the current response file is missing the new property, so for now we expect a not supported error 38 | def test_getPowerConsumptionDays(self): 39 | self.assertRaises(PyViCareNotSupportedFeatureError, self.device.getPowerConsumptionDays) 40 | 41 | def test_getFrostProtectionActive(self): 42 | self.assertEqual( 43 | self.device.circuits[0].getFrostProtectionActive(), False) 44 | 45 | def test_getDomesticHotWaterCirculationPumpActive(self): 46 | self.assertEqual( 47 | self.device.getDomesticHotWaterCirculationPumpActive(), False) 48 | 49 | def test_getDomesticHotWaterOutletTemperature(self): 50 | self.assertEqual( 51 | self.device.getDomesticHotWaterOutletTemperature(), 29.8) 52 | 53 | def test_getDomesticHotWaterCirculationScheduleModes(self): 54 | self.assertEqual( 55 | self.device.getDomesticHotWaterCirculationScheduleModes(), ['on']) 56 | 57 | def test_getOutsideTemperature(self): 58 | self.assertEqual( 59 | self.device.getOutsideTemperature(), 26.2) 60 | 61 | def test_getBoilerTemperature(self): 62 | self.assertEqual( 63 | self.device.getBoilerTemperature(), 35) 64 | -------------------------------------------------------------------------------- /tests/test_VitolaUniferral.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareOilBoiler import OilBoiler 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class VitolaUniferral(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/VitolaUniferral.json') 10 | self.device = OilBoiler(self.service) 11 | 12 | def test_getDomesticHotWaterConfiguredTemperature(self): 13 | self.assertEqual( 14 | self.device.getDomesticHotWaterConfiguredTemperature(), 60) 15 | 16 | def test_getActive(self): 17 | self.assertEqual(self.device.burners[0].getActive(), True) 18 | 19 | def test_getBurnerStarts(self): 20 | self.assertEqual(self.device.burners[0].getStarts(), 5156) 21 | 22 | def test_getBurnerHours(self): 23 | self.assertEqual(self.device.burners[0].getHours(), 1021.4) 24 | 25 | def test_getBoilerTemperature(self): 26 | self.assertEqual(self.device.getBoilerTemperature(), 26.6) 27 | 28 | @unittest.skip("dump is not up to date, underlying data point was rernamed") 29 | def test_getDomesticHotWaterStorageTemperature(self): 30 | self.assertEqual(self.device.getDomesticHotWaterStorageTemperature(), 56.9) 31 | -------------------------------------------------------------------------------- /tests/test_Vitopure350.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError 4 | from PyViCare.PyViCareVentilationDevice import VentilationDevice 5 | from tests.ViCareServiceMock import ViCareServiceMock 6 | 7 | 8 | class Vitopure350(unittest.TestCase): 9 | def setUp(self): 10 | self.service = ViCareServiceMock('response/Vitopure350.json') 11 | self.device = VentilationDevice(self.service) 12 | 13 | def test_getActiveMode(self): 14 | self.assertEqual("sensorDriven", self.device.getActiveMode()) 15 | 16 | def test_getAvailableModes(self): 17 | expected_modes = ['permanent', 'ventilation', 'sensorDriven'] 18 | self.assertListEqual(expected_modes, self.device.getAvailableModes()) 19 | 20 | def test_getActiveProgram(self): 21 | self.assertEqual("automatic", self.device.getActiveProgram()) 22 | 23 | def test_getAvailablePrograms(self): 24 | expected_programs = ['standby'] 25 | self.assertListEqual(expected_programs, self.device.getAvailablePrograms()) 26 | 27 | def test_getPermanentLevels(self): 28 | expected_levels = ['levelOne', 'levelTwo', 'levelThree', 'levelFour'] 29 | self.assertListEqual(expected_levels, self.device.getPermanentLevels()) 30 | 31 | def test_getSchedule(self): 32 | keys = ['active', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] 33 | self.assertListEqual(keys, list(self.device.getSchedule().keys())) 34 | 35 | def test_getSerial(self): 36 | with self.assertRaises(PyViCareNotSupportedFeatureError): 37 | self.device.getSerial() 38 | 39 | def test_getActiveVentilationMode(self): 40 | self.assertEqual("sensorDriven", self.device.getActiveVentilationMode()) 41 | 42 | def test_getVentilationModes(self): 43 | expected_modes = ['permanent', 'ventilation', 'sensorDriven'] 44 | self.assertListEqual(expected_modes, self.device.getVentilationModes()) 45 | 46 | def test_getVentilationMode(self): 47 | self.assertEqual(False, self.device.getVentilationMode("filterChange")) 48 | 49 | def test_getVentilationLevels(self): 50 | expected_levels = ['levelOne', 'levelTwo', 'levelThree', 'levelFour'] 51 | self.assertListEqual(expected_levels, self.device.getVentilationLevels()) 52 | 53 | def test_ventilationState(self): 54 | self.assertEqual(self.device.getVentilationDemand(), "unknown") 55 | self.assertEqual(self.device.getVentilationLevel(), "unknown") 56 | self.assertEqual(self.device.getVentilationReason(), "sensorDriven") 57 | 58 | def test_ventilationQuickmode(self): 59 | self.assertEqual(self.device.getVentilationQuickmode("standby"), False) 60 | 61 | def test_ventilationQuickmodes(self): 62 | self.assertEqual(self.device.getVentilationQuickmodes(), [ 63 | "forcedLevelFour", 64 | "standby", 65 | "silent", 66 | ]) 67 | 68 | def test_activateVentilationQuickmodeStandby(self): 69 | self.device.activateVentilationQuickmode("standby") 70 | self.assertEqual(len(self.service.setPropertyData), 1) 71 | self.assertEqual(self.service.setPropertyData[0]['action'], 'activate') 72 | self.assertEqual(self.service.setPropertyData[0]['property_name'], 'ventilation.quickmodes.standby') 73 | 74 | def test_deactivateVentilationQuickmodeStandby(self): 75 | self.device.deactivateVentilationQuickmode("standby") 76 | self.assertEqual(len(self.service.setPropertyData), 1) 77 | self.assertEqual(self.service.setPropertyData[0]['action'], 'deactivate') 78 | self.assertEqual(self.service.setPropertyData[0]['property_name'], 'ventilation.quickmodes.standby') 79 | -------------------------------------------------------------------------------- /tests/test_device_error.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareDevice import Device 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class DeviceErrorTest(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/deviceerrors/F.1100.json') 10 | self.device = Device(self.service) 11 | 12 | def test_deviceErrors(self): 13 | errors = self.device.getDeviceErrors() 14 | self.assertEqual(len(errors), 1) 15 | self.assertEqual(errors[0]["errorCode"], "F.1100") 16 | self.assertEqual(errors[0]["priority"], "criticalError") 17 | -------------------------------------------------------------------------------- /tests/test_vitocal-with-vitovent.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareHeatPump import HeatPump 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class Vitocal_with_Vitovent(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/Vitocal-200S-with-Vitovent-300W.json') 10 | self.device = HeatPump(self.service) 11 | 12 | def test_isDomesticHotWaterDevice(self): 13 | self.assertEqual(self.device.isDomesticHotWaterDevice(), True) 14 | 15 | def test_isSolarThermalDevice(self): 16 | self.assertEqual(self.device.isSolarThermalDevice(), False) 17 | 18 | def test_isVentilationDevice(self): 19 | self.assertEqual(self.device.isVentilationDevice(), True) 20 | 21 | def test_getActiveVentilationMode(self): 22 | self.assertEqual("ventilation", self.device.getActiveVentilationMode()) 23 | 24 | def test_getVentilationModes(self): 25 | expected_modes = ['standby', 'standard', 'ventilation'] 26 | self.assertListEqual(expected_modes, self.device.getVentilationModes()) 27 | 28 | def test_getVentilationMode(self): 29 | self.assertEqual(False, self.device.getVentilationMode("standby")) 30 | 31 | def test_ventilation_state(self): 32 | self.assertEqual(self.device.getVentilationDemand(), "ventilation") 33 | self.assertEqual(self.device.getVentilationLevel(), "levelTwo") 34 | self.assertEqual(self.device.getVentilationReason(), "schedule") 35 | 36 | def test_ventilationQuickmode(self): 37 | self.assertEqual(self.device.getVentilationQuickmode("comfort"), False) 38 | self.assertEqual(self.device.getVentilationQuickmode("eco"), False) 39 | self.assertEqual(self.device.getVentilationQuickmode("holiday"), False) 40 | 41 | def test_ventilationQuickmodes(self): 42 | self.assertEqual(self.device.getVentilationQuickmodes(), [ 43 | "comfort", 44 | "eco", 45 | "holiday", 46 | ]) 47 | -------------------------------------------------------------------------------- /tests/test_zigbee_cs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareRoomSensor import RoomSensor 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class ZK03839(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/zigbee_Smart_cs_generic_50.json') 10 | self.device = RoomSensor(self.service) 11 | 12 | def test_getSerial(self): 13 | self.assertEqual(self.device.getSerial(), "zigbee-f082c0fffe43d8cd") 14 | 15 | def test_isDomesticHotWaterDevice(self): 16 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 17 | 18 | def test_isSolarThermalDevice(self): 19 | self.assertEqual(self.device.isSolarThermalDevice(), False) 20 | 21 | def test_isVentilationDevice(self): 22 | self.assertEqual(self.device.isVentilationDevice(), False) 23 | 24 | def test_getTemperature(self): 25 | self.assertEqual(self.device.getTemperature(), 15) 26 | 27 | def test_getHumidity(self): 28 | self.assertEqual(self.device.getHumidity(), 37) 29 | 30 | def test_getBatteryLevel(self): 31 | self.assertEqual(self.device.getBatteryLevel(), 89) 32 | -------------------------------------------------------------------------------- /tests/test_zigbee_eTRV.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareRadiatorActuator import RadiatorActuator 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class ZK03840(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/zigbee_Smart_Device_eTRV_generic_50.json') 10 | self.device = RadiatorActuator(self.service) 11 | 12 | def test_getSerial(self): 13 | self.assertEqual(self.device.getSerial(), "zigbee-048727fffeb429ae") 14 | 15 | def test_isDomesticHotWaterDevice(self): 16 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 17 | 18 | def test_isSolarThermalDevice(self): 19 | self.assertEqual(self.device.isSolarThermalDevice(), False) 20 | 21 | def test_isVentilationDevice(self): 22 | self.assertEqual(self.device.isVentilationDevice(), False) 23 | 24 | def test_getTemperature(self): 25 | self.assertEqual(self.device.getTemperature(), 16.5) 26 | 27 | def test_getBatteryLevel(self): 28 | self.assertEqual(self.device.getBatteryLevel(), 66) 29 | 30 | def test_getTargetTemperature(self): 31 | self.assertEqual(self.device.getTargetTemperature(), 8) 32 | 33 | def test_setTargetTemperature(self): 34 | self.device.setTargetTemperature(22) 35 | self.assertEqual(len(self.service.setPropertyData), 1) 36 | self.assertEqual(self.service.setPropertyData[0]['property_name'], 'trv.temperature') 37 | self.assertEqual(self.service.setPropertyData[0]['action'], 'setTargetTemperature') 38 | self.assertEqual(self.service.setPropertyData[0]['data'], {'temperature': 22}) 39 | -------------------------------------------------------------------------------- /tests/test_zigbee_zk03839.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareRoomSensor import RoomSensor 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class ZK03839(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/zigbee_zk03839.json') 10 | self.device = RoomSensor(self.service) 11 | 12 | def test_getSerial(self): 13 | self.assertEqual(self.device.getSerial(), "zigbee-2c1165fffe977770") 14 | 15 | def test_isDomesticHotWaterDevice(self): 16 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 17 | 18 | def test_isSolarThermalDevice(self): 19 | self.assertEqual(self.device.isSolarThermalDevice(), False) 20 | 21 | def test_isVentilationDevice(self): 22 | self.assertEqual(self.device.isVentilationDevice(), False) 23 | 24 | def test_getTemperature(self): 25 | self.assertEqual( 26 | self.device.getTemperature(), 19.7) 27 | 28 | def test_getHumidity(self): 29 | self.assertEqual( 30 | self.device.getHumidity(), 56) 31 | -------------------------------------------------------------------------------- /tests/test_zigbee_zk03840.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from PyViCare.PyViCareRadiatorActuator import RadiatorActuator 4 | from tests.ViCareServiceMock import ViCareServiceMock 5 | 6 | 7 | class ZK03840(unittest.TestCase): 8 | def setUp(self): 9 | self.service = ViCareServiceMock('response/zigbee_zk03840_trv.json') 10 | self.device = RadiatorActuator(self.service) 11 | 12 | def test_getSerial(self): 13 | self.assertEqual(self.device.getSerial(), "zigbee-048727fffe196e03") 14 | 15 | def test_isDomesticHotWaterDevice(self): 16 | self.assertEqual(self.device.isDomesticHotWaterDevice(), False) 17 | 18 | def test_isSolarThermalDevice(self): 19 | self.assertEqual(self.device.isSolarThermalDevice(), False) 20 | 21 | def test_isVentilationDevice(self): 22 | self.assertEqual(self.device.isVentilationDevice(), False) 23 | 24 | def test_getTemperature(self): 25 | self.assertEqual( 26 | self.device.getTemperature(), 18.4) 27 | 28 | def test_getTargetTemperature(self): 29 | self.assertEqual( 30 | self.device.getTargetTemperature(), 8) 31 | 32 | def test_setTargetTemperature(self): 33 | self.device.setTargetTemperature(22) 34 | self.assertEqual(len(self.service.setPropertyData), 1) 35 | self.assertEqual( 36 | self.service.setPropertyData[0]['property_name'], 'trv.temperature') 37 | self.assertEqual( 38 | self.service.setPropertyData[0]['action'], 'setTargetTemperature') 39 | self.assertEqual(self.service.setPropertyData[0]['data'], { 40 | 'temperature': 22}) 41 | --------------------------------------------------------------------------------