├── src └── p1monitor │ ├── py.typed │ ├── exceptions.py │ ├── __init__.py │ ├── p1monitor.py │ └── models.py ├── tests ├── fixtures │ ├── no_data.json │ ├── watermeter.json │ ├── smartmeter.json │ ├── settings.json │ └── phases.json ├── __init__.py ├── ruff.toml ├── conftest.py ├── __snapshots__ │ └── test_models.ambr ├── test_exceptions.py ├── test_p1monitor.py └── test_models.py ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature_request.yml │ └── bug_report.yml ├── workflows │ ├── release-drafter.yaml │ ├── lock.yaml │ ├── sync-labels.yaml │ ├── pr-labels.yaml │ ├── typing.yaml │ ├── stale.yaml │ ├── release.yaml │ ├── tests.yaml │ └── linting.yaml ├── PULL_REQUEST_TEMPLATE.md ├── renovate.json ├── release-drafter.yml └── labels.yml ├── examples ├── __init__.py ├── ruff.toml ├── watermeter.py ├── settings.py ├── smartmeter.py └── phases.py ├── .gitattributes ├── assets └── header_p1monitor-min.png ├── LICENSE ├── CONTRIBUTING.md ├── .yamllint ├── .gitignore ├── .devcontainer └── devcontainer.json ├── .pre-commit-config.yaml ├── pyproject.toml ├── CODE_OF_CONDUCT.md └── README.md /src/p1monitor/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/no_data.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | .github/* @klaasnicolaas 2 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | """Examples for this library.""" 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.py whitespace=error 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | github: klaasnicolaas 3 | ko_fi: klaasnicolaas 4 | -------------------------------------------------------------------------------- /assets/header_p1monitor-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klaasnicolaas/python-p1monitor/HEAD/assets/header_p1monitor-min.png -------------------------------------------------------------------------------- /examples/ruff.toml: -------------------------------------------------------------------------------- 1 | # This extend our general Ruff rules specifically for the examples 2 | extend = "../pyproject.toml" 3 | 4 | lint.extend-ignore = [ 5 | "T201", # Allow the use of print() in examples 6 | ] 7 | -------------------------------------------------------------------------------- /tests/fixtures/watermeter.json: -------------------------------------------------------------------------------- 1 | [{"TIMEPERIOD_ID": 13, "TIMESTAMP_UTC": 1644620400, "TIMESTAMP_lOCAL": "2022-02-12 00:00:00", "WATERMETER_CONSUMPTION_LITER": 128.0, "WATERMETER_CONSUMPTION_TOTAL_M3": 1640.399, "WATERMETER_PULS_COUNT": 128.0}] 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Asynchronous Python client for the P1 Monitor.""" 2 | 3 | from pathlib import Path 4 | 5 | 6 | def load_fixtures(filename: str) -> str: 7 | """Load a fixture.""" 8 | path = Path(__file__).parent / "fixtures" / filename 9 | return path.read_text() 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: true 3 | contact_links: 4 | - name: ❓ Ask a Question 5 | url: https://github.com/klaasnicolaas/python-p1monitor/discussions/new/choose 6 | about: We use GitHub issues for tracking bugs / feature requests, check discussions for questions. 7 | -------------------------------------------------------------------------------- /tests/fixtures/smartmeter.json: -------------------------------------------------------------------------------- 1 | [{"CONSUMPTION_GAS_M3": 2289.967, "CONSUMPTION_KWH_HIGH": 2996.141, "CONSUMPTION_KWH_LOW": 5436.256, "CONSUMPTION_W": 935, "PRODUCTION_KWH_HIGH": 4408.947, "PRODUCTION_KWH_LOW": 1575.502, "PRODUCTION_W": 0, "RECORD_IS_PROCESSED": 0, "TARIFCODE": "D", "TIMESTAMP_UTC": 1633130812, "TIMESTAMP_lOCAL": "2021-10-02 01:26:52"}] 2 | -------------------------------------------------------------------------------- /src/p1monitor/exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions for P1 Monitor.""" 2 | 3 | 4 | class P1MonitorError(Exception): 5 | """Generic P1 Monitor exception.""" 6 | 7 | 8 | class P1MonitorConnectionError(P1MonitorError): 9 | """P1 Monitor connection exception.""" 10 | 11 | 12 | class P1MonitorNoDataError(P1MonitorError): 13 | """P1 Monitor no data exception.""" 14 | -------------------------------------------------------------------------------- /tests/ruff.toml: -------------------------------------------------------------------------------- 1 | # This extend our general Ruff rules specifically for tests 2 | extend = "../pyproject.toml" 3 | 4 | lint.extend-select = [ 5 | "PT", # Use @pytest.fixture without parentheses 6 | ] 7 | 8 | lint.extend-ignore = [ 9 | "S101", # Use of assert detected. As these are tests... 10 | "SLF001", # Tests will access private/protected members... 11 | "TC002", # pytest doesn't like this one... 12 | ] 13 | -------------------------------------------------------------------------------- /src/p1monitor/__init__.py: -------------------------------------------------------------------------------- 1 | """Asynchronous Python client for the P1 Monitor API.""" 2 | 3 | from .exceptions import P1MonitorConnectionError, P1MonitorError, P1MonitorNoDataError 4 | from .models import Phases, Settings, SmartMeter, WaterMeter 5 | from .p1monitor import P1Monitor 6 | 7 | __all__ = [ 8 | "P1Monitor", 9 | "P1MonitorConnectionError", 10 | "P1MonitorError", 11 | "P1MonitorNoDataError", 12 | "Phases", 13 | "Settings", 14 | "SmartMeter", 15 | "WaterMeter", 16 | ] 17 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Drafter 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update_release_draft: 13 | name: ✏️ Draft release 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | pull-requests: read 18 | steps: 19 | - name: 🚀 Run Release Drafter 20 | uses: release-drafter/release-drafter@v6.1.0 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Fixture for the P1Monitor tests.""" 2 | 3 | from collections.abc import AsyncGenerator 4 | 5 | import pytest 6 | from aiohttp import ClientSession 7 | 8 | from p1monitor import P1Monitor 9 | 10 | 11 | @pytest.fixture(name="p1monitor_client") 12 | async def client() -> AsyncGenerator[P1Monitor, None]: 13 | """Return a P1Monitor client.""" 14 | async with ( 15 | ClientSession() as session, 16 | P1Monitor(host="192.168.1.2", port=80, session=session) as p1monitor_client, 17 | ): 18 | yield p1monitor_client 19 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lock 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | schedule: 7 | - cron: "0 3 * * *" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | inactivity-lock: 12 | name: Lock issues and PRs 13 | runs-on: ubuntu-latest 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | steps: 18 | - name: 🔒 Lock closed issues and PRs 19 | uses: klaasnicolaas/action-inactivity-lock@v1.1.3 20 | with: 21 | days-inactive-issues: 30 22 | lock-reason-issues: "" 23 | days-inactive-prs: 1 24 | lock-reason-prs: "" 25 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sync labels 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - .github/labels.yml 11 | workflow_dispatch: 12 | 13 | jobs: 14 | labels: 15 | name: ♻️ Sync labels 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - name: ⤵️ Check out code from GitHub 21 | uses: actions/checkout@v6.0.1 22 | - name: 🚀 Run Label Syncer 23 | uses: micnncim/action-label-syncer@v1.3.0 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/pr-labels.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: PR Labels 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | pull_request_target: 7 | types: 8 | - opened 9 | - labeled 10 | - unlabeled 11 | - synchronize 12 | workflow_call: 13 | 14 | jobs: 15 | validate: 16 | name: Verify 17 | runs-on: ubuntu-latest 18 | permissions: 19 | contents: read 20 | pull-requests: read 21 | steps: 22 | - name: 🏷 Verify PR has a valid label 23 | uses: klaasnicolaas/action-pr-labels@v2.0.2 24 | with: 25 | valid-labels: >- 26 | breaking-change, bugfix, documentation, enhancement, sync, 27 | refactor, performance, new-feature, maintenance, ci, dependencies 28 | -------------------------------------------------------------------------------- /examples/watermeter.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0621 2 | """Asynchronous Python client for the P1 Monitor API.""" 3 | 4 | import asyncio 5 | 6 | from p1monitor import P1Monitor, WaterMeter 7 | 8 | 9 | async def main() -> None: 10 | """Show example on getting P1 Monitor data.""" 11 | async with P1Monitor(host="127.0.0.1") as client: 12 | watermeter: WaterMeter = await client.watermeter() 13 | 14 | print(watermeter) 15 | print() 16 | print("--- P1 Monitor | WaterMeter ---") 17 | print(f"Consumption Day: {watermeter.consumption_day}") 18 | print(f"Consumption Total: {watermeter.consumption_total}") 19 | print(f"Pulse Count: {watermeter.pulse_count}") 20 | 21 | 22 | if __name__ == "__main__": 23 | asyncio.run(main()) 24 | -------------------------------------------------------------------------------- /tests/fixtures/settings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "CONFIGURATION_ID": 1, 4 | "LABEL": "Verbruik tarief elektriciteit dal/nacht in euro.", 5 | "PARAMETER": "0.22311" 6 | }, 7 | { 8 | "CONFIGURATION_ID": 2, 9 | "LABEL": "Verbruik tarief elektriciteit piek/dag in euro.", 10 | "PARAMETER": "0.24388" 11 | }, 12 | { 13 | "CONFIGURATION_ID": 3, 14 | "LABEL": "Geleverd tarief elektriciteit dal/nacht in euro.", 15 | "PARAMETER": "0.22311" 16 | }, 17 | { 18 | "CONFIGURATION_ID": 4, 19 | "LABEL": "Geleverd tarief elektriciteit piek/dag in euro.", 20 | "PARAMETER": "0.24388" 21 | }, 22 | { 23 | "CONFIGURATION_ID": 15, 24 | "LABEL": "Verbruik tarief gas in euro.", 25 | "PARAMETER": "0.86687" 26 | } 27 | ] 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Proposed change 2 | 8 | 9 | ## Additional information 10 | 14 | 15 | - This PR fixes or closes issue: fixes # 16 | 17 | ## Checklist 18 | 22 | 23 | - [ ] I have updated the documentation if needed. 24 | - [ ] I have updated the tests if needed. 25 | -------------------------------------------------------------------------------- /tests/__snapshots__/test_models.ambr: -------------------------------------------------------------------------------- 1 | # serializer version: 1 2 | # name: test_phases 3 | Phases(voltage_phase_l1=229.0, voltage_phase_l2=0.0, voltage_phase_l3=0.0, current_phase_l1=4.0, current_phase_l2=0.0, current_phase_l3=0.0, power_consumed_phase_l1=863, power_consumed_phase_l2=0, power_consumed_phase_l3=0, power_produced_phase_l1=0, power_produced_phase_l2=0, power_produced_phase_l3=0) 4 | # --- 5 | # name: test_settings 6 | Settings(gas_consumption_price=0.86687, energy_consumption_price_high=0.24388, energy_consumption_price_low=0.22311, energy_production_price_high=0.24388, energy_production_price_low=0.22311) 7 | # --- 8 | # name: test_smartmeter 9 | SmartMeter(gas_consumption=2289.967, energy_tariff_period=, power_consumption=935, energy_consumption_high=2996.141, energy_consumption_low=5436.256, power_production=0, energy_production_high=4408.947, energy_production_low=1575.502) 10 | # --- 11 | # name: test_watermeter 12 | WaterMeter(consumption_day=128.0, consumption_total=1640.399, pulse_count=128.0) 13 | # --- 14 | -------------------------------------------------------------------------------- /.github/workflows/typing.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Typing 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | DEFAULT_PYTHON: "3.12" 12 | 13 | jobs: 14 | mypy: 15 | name: mypy 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | steps: 20 | - name: ⤵️ Check out code from GitHub 21 | uses: actions/checkout@v6.0.1 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@v6.1.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 examples src tests 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2021-2025 Klaas Schoute 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/settings.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0621 2 | """Asynchronous Python client for the P1 Monitor API.""" 3 | 4 | import asyncio 5 | 6 | from p1monitor import P1Monitor, Settings 7 | 8 | 9 | async def main() -> None: 10 | """Show example on getting P1 Monitor data.""" 11 | async with P1Monitor(host="127.0.0.1") as client: 12 | settings: Settings = await client.settings() 13 | 14 | print(settings) 15 | energy_cons_high = settings.energy_consumption_price_high 16 | energy_cons_low = settings.energy_consumption_price_low 17 | energy_prod_high = settings.energy_production_price_high 18 | energy_prod_low = settings.energy_production_price_low 19 | print() 20 | print("--- P1 Monitor | Settings ---") 21 | print(f"Gas Price: {settings.gas_consumption_price}") 22 | print(f"Energy Consumption Price - High: {energy_cons_high}") 23 | print(f"Energy Consumption Price - Low: {energy_cons_low}") 24 | print(f"Energy Production Price - High: {energy_prod_high}") 25 | print(f"Energy Production Price - Low: {energy_prod_low}") 26 | 27 | 28 | if __name__ == "__main__": 29 | asyncio.run(main()) 30 | -------------------------------------------------------------------------------- /examples/smartmeter.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0621 2 | """Asynchronous Python client for the P1 Monitor API.""" 3 | 4 | import asyncio 5 | 6 | from p1monitor import P1Monitor, SmartMeter 7 | 8 | 9 | async def main() -> None: 10 | """Show example on getting P1 Monitor data.""" 11 | async with P1Monitor(host="127.0.0.1") as client: 12 | smartmeter: SmartMeter = await client.smartmeter() 13 | 14 | print(smartmeter) 15 | print() 16 | print("--- P1 Monitor | SmartMeter ---") 17 | print(f"Power Consumption: {smartmeter.power_consumption}") 18 | print(f"Energy Consumption - High: {smartmeter.energy_consumption_high}") 19 | print(f"Energy Consumption - Low: {smartmeter.energy_consumption_low}") 20 | print() 21 | print(f"Power Production: {smartmeter.power_production}") 22 | print(f"Energy Production - High: {smartmeter.energy_production_high}") 23 | print(f"Energy Production - Low: {smartmeter.energy_production_low}") 24 | print(f"Energy Tariff: {smartmeter.energy_tariff_period}") 25 | print() 26 | print(f"Gas Consumption: {smartmeter.gas_consumption}") 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run(main()) 31 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "timezone": "Europe/Amsterdam", 4 | "schedule": ["before 6am every weekday"], 5 | "rebaseWhen": "behind-base-branch", 6 | "dependencyDashboard": true, 7 | "labels": ["dependencies"], 8 | "lockFileMaintenance": { 9 | "enabled": true, 10 | "automerge": true 11 | }, 12 | "commitMessagePrefix": "⬆️", 13 | "packageRules": [ 14 | { 15 | "matchManagers": ["poetry"], 16 | "addLabels": ["python"] 17 | }, 18 | { 19 | "matchManagers": ["poetry"], 20 | "matchDepTypes": ["dev"], 21 | "rangeStrategy": "pin" 22 | }, 23 | { 24 | "matchManagers": ["poetry"], 25 | "matchUpdateTypes": ["minor", "patch"], 26 | "automerge": true 27 | }, 28 | { 29 | "matchManagers": ["github-actions"], 30 | "addLabels": ["github_actions"], 31 | "rangeStrategy": "pin", 32 | "extractVersion": "^(?v\\d+\\.\\d+\\.\\d+)$", 33 | "versioning": "regex:^v(?\\d+)(\\.(?\\d+)\\.(?\\d+))?$" 34 | }, 35 | { 36 | "matchManagers": ["github-actions"], 37 | "matchUpdateTypes": ["minor", "patch"], 38 | "automerge": true 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish 4 | to make via issue, email, or any other method with the owners of this repository 5 | before making a change. 6 | 7 | Please note we have a code of conduct, please follow it in all your interactions 8 | with the project. 9 | 10 | ## Issues and feature requests 11 | 12 | You've found a bug in the source code, a mistake in the documentation or maybe 13 | you'd like a new feature? You can help us by submitting an issue to our 14 | [GitHub Repository][github]. Before you create an issue, make sure you search 15 | the archive, maybe your question was already answered. 16 | 17 | Even better: You could submit a pull request with a fix / new feature! 18 | 19 | ## Pull request process 20 | 21 | 1. Search our repository for open or closed [pull requests][prs] that relates 22 | to your submission. You don't want to duplicate effort. 23 | 24 | 1. You may merge the pull request in once you have the sign-off of two other 25 | developers, or if you do not have permission to do that, you may request 26 | the second reviewer to merge it for you. 27 | 28 | [github]: https://github.com/klaasnicolaas/python-p1monitor/issues 29 | [prs]: https://github.com/klaasnicolaas/python-p1monitor/pulls 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚀 Feature Request 3 | description: Request a new feature or enhancement 4 | title: "" 5 | labels: 6 | - enhancement 7 | - new-feature 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please search to see if an issue already exists for the feature you want. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | 17 | - type: textarea 18 | attributes: 19 | label: How would this feature be useful? 20 | description: Describe any use cases this solves or frustrations it alleviates. 21 | validations: 22 | required: false 23 | 24 | - type: textarea 25 | attributes: 26 | label: Describe the solution you'd like 27 | description: If you have an idea on how to do this, let us know here! 28 | validations: 29 | required: false 30 | 31 | - type: textarea 32 | attributes: 33 | label: Describe alternatives you've considered 34 | description: If there's some workaround or alternative solutions, let us know here! 35 | validations: 36 | required: false 37 | 38 | - type: textarea 39 | attributes: 40 | label: Anything else? 41 | description: Any other relevant information or background. 42 | validations: 43 | required: false 44 | -------------------------------------------------------------------------------- /examples/phases.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0621 2 | """Asynchronous Python client for the P1 Monitor API.""" 3 | 4 | import asyncio 5 | 6 | from p1monitor import P1Monitor, Phases 7 | 8 | 9 | async def main() -> None: 10 | """Show example on getting P1 Monitor data.""" 11 | async with P1Monitor(host="127.0.0.1") as client: 12 | phases: Phases = await client.phases() 13 | 14 | print(phases) 15 | print() 16 | print("--- P1 Monitor | Phases ---") 17 | print(f"Voltage Phase L1: {phases.voltage_phase_l1}") 18 | print(f"Voltage Phase L2: {phases.voltage_phase_l2}") 19 | print(f"Voltage Phase L3: {phases.voltage_phase_l3}") 20 | print() 21 | print(f"Current Phase L1: {phases.current_phase_l1}") 22 | print(f"Current Phase L2: {phases.current_phase_l2}") 23 | print(f"Current Phase L3: {phases.current_phase_l3}") 24 | print() 25 | print(f"Power Consumed Phase L1: {phases.power_consumed_phase_l1}") 26 | print(f"Power Consumed Phase L2: {phases.power_consumed_phase_l2}") 27 | print(f"Power Consumed Phase L3: {phases.power_consumed_phase_l3}") 28 | print() 29 | print(f"Power Produced Phase L1: {phases.power_produced_phase_l1}") 30 | print(f"Power Produced Phase L2: {phases.power_produced_phase_l2}") 31 | print(f"Power Produced Phase L3: {phases.power_produced_phase_l3}") 32 | 33 | 34 | if __name__ == "__main__": 35 | asyncio.run(main()) 36 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | ignore: | 3 | .venv/lib 4 | .copier-answers.yml 5 | rules: 6 | braces: 7 | level: error 8 | min-spaces-inside: 0 9 | max-spaces-inside: 1 10 | min-spaces-inside-empty: -1 11 | max-spaces-inside-empty: -1 12 | brackets: 13 | level: error 14 | min-spaces-inside: 0 15 | max-spaces-inside: 0 16 | min-spaces-inside-empty: -1 17 | max-spaces-inside-empty: -1 18 | colons: 19 | level: error 20 | max-spaces-before: 0 21 | max-spaces-after: 1 22 | commas: 23 | level: error 24 | max-spaces-before: 0 25 | min-spaces-after: 1 26 | max-spaces-after: 1 27 | comments: 28 | level: error 29 | require-starting-space: true 30 | min-spaces-from-content: 1 31 | comments-indentation: 32 | level: error 33 | document-end: 34 | level: error 35 | present: false 36 | document-start: 37 | level: error 38 | present: true 39 | empty-lines: 40 | level: error 41 | max: 1 42 | max-start: 0 43 | max-end: 1 44 | hyphens: 45 | level: error 46 | max-spaces-after: 1 47 | indentation: 48 | level: error 49 | spaces: 2 50 | indent-sequences: true 51 | check-multi-line-strings: false 52 | key-duplicates: 53 | level: error 54 | line-length: 55 | level: warning 56 | max: 120 57 | allow-non-breakable-words: true 58 | allow-non-breakable-inline-mappings: true 59 | new-line-at-end-of-file: 60 | level: error 61 | new-lines: 62 | level: error 63 | type: unix 64 | trailing-spaces: 65 | level: error 66 | truthy: 67 | level: error 68 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: "v$RESOLVED_VERSION" 3 | tag-template: "v$RESOLVED_VERSION" 4 | change-template: "- #$NUMBER $TITLE @$AUTHOR" 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 | collapse-after: 5 31 | labels: 32 | - "dependencies" 33 | 34 | exclude-labels: 35 | - "sync" 36 | 37 | version-resolver: 38 | major: 39 | labels: 40 | - "major" 41 | - "breaking-change" 42 | minor: 43 | labels: 44 | - "minor" 45 | - "new-feature" 46 | patch: 47 | labels: 48 | - "bugfix" 49 | - "chore" 50 | - "ci" 51 | - "dependencies" 52 | - "documentation" 53 | - "enhancement" 54 | - "performance" 55 | - "refactor" 56 | default: patch 57 | 58 | template: | 59 | ## What's changed 60 | 61 | _To receive a notification on new releases, click on **Watch** > **Custom** > **Releases** on the top._ 62 | 63 | $CHANGES 64 | 65 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 66 | -------------------------------------------------------------------------------- /.github/workflows/stale.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Stale 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | schedule: 7 | - cron: "0 1 * * *" 8 | workflow_dispatch: 9 | 10 | jobs: 11 | stale: 12 | name: 🧹 Clean up stale issues and PRs 13 | runs-on: ubuntu-latest 14 | permissions: 15 | issues: write 16 | pull-requests: write 17 | steps: 18 | - name: 🚀 Run stale 19 | uses: actions/stale@v10.1.1 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | days-before-stale: 30 23 | days-before-close: 7 24 | remove-stale-when-updated: true 25 | stale-issue-label: "stale" 26 | exempt-issue-labels: "no-stale,help-wanted" 27 | stale-issue-message: > 28 | There hasn't been any activity on this issue recently, so we 29 | clean up some of the older and inactive issues. 30 | 31 | Please make sure to update to the latest version and 32 | check if that solves the issue. Let us know if that works for you 33 | by leaving a comment 👍 34 | 35 | This issue has now been marked as stale and will be closed if no 36 | further activity occurs. Thanks! 37 | stale-pr-label: "stale" 38 | exempt-pr-labels: "no-stale" 39 | stale-pr-message: > 40 | There hasn't been any activity on this pull request recently. This 41 | pull request has been automatically marked as stale because of that 42 | and will be closed if no further activity occurs within 7 days. 43 | Thank you for your contributions. 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug Report 3 | description: File a bug/issue 4 | title: "<title>" 5 | labels: 6 | - bug 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: Please search to see if an issue already exists for the bug you encountered. 13 | options: 14 | - label: I have searched the existing issues 15 | required: true 16 | 17 | - type: textarea 18 | attributes: 19 | label: Current Behavior 20 | description: A concise description of what you're experiencing. 21 | validations: 22 | required: false 23 | 24 | - type: textarea 25 | attributes: 26 | label: Expected Behavior 27 | description: A concise description of what you expected to happen. 28 | validations: 29 | required: false 30 | 31 | - type: textarea 32 | attributes: 33 | label: Steps To Reproduce 34 | description: Steps to reproduce the behavior. 35 | placeholder: | 36 | 1. In this environment... 37 | 2. With this config... 38 | 3. Run '...' 39 | 4. See error... 40 | validations: 41 | required: false 42 | 43 | - type: textarea 44 | attributes: 45 | label: Environment 46 | description: | 47 | Please describe your execution environment providing as much detail as possible 48 | render: Markdown 49 | validations: 50 | required: false 51 | 52 | - type: textarea 53 | attributes: 54 | label: Anything else? 55 | description: | 56 | Links? References? Anything that will give us more context about the issue you are encountering! 57 | 58 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 59 | validations: 60 | required: false 61 | -------------------------------------------------------------------------------- /tests/fixtures/phases.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "LABEL": "Huidige KW verbruik L1 (21.7.0)", 4 | "SECURITY": 0, 5 | "STATUS": "0.863", 6 | "STATUS_ID": 74 7 | }, 8 | { 9 | "LABEL": "Huidige KW verbruik L2 (41.7.0)", 10 | "SECURITY": 0, 11 | "STATUS": "0.0", 12 | "STATUS_ID": 75 13 | }, 14 | { 15 | "LABEL": "Huidige KW verbruik L3 (61.7.0)", 16 | "SECURITY": 0, 17 | "STATUS": "0.0", 18 | "STATUS_ID": 76 19 | }, 20 | { 21 | "LABEL": "Huidige KW levering L1 (22.7.0)", 22 | "SECURITY": 0, 23 | "STATUS": "0.0", 24 | "STATUS_ID": 77 25 | }, 26 | { 27 | "LABEL": "Huidige KW levering L2 (42.7.0)", 28 | "SECURITY": 0, 29 | "STATUS": "0.0", 30 | "STATUS_ID": 78 31 | }, 32 | { 33 | "LABEL": "Huidige KW levering L3 (62.7.0)", 34 | "SECURITY": 0, 35 | "STATUS": "0.0", 36 | "STATUS_ID": 79 37 | }, 38 | { 39 | "LABEL": "Huidige Amperage L1 (31.7.0)", 40 | "SECURITY": 0, 41 | "STATUS": "4.0", 42 | "STATUS_ID": 100 43 | }, 44 | { 45 | "LABEL": "Huidige Amperage L2 (51.7.0)", 46 | "SECURITY": 0, 47 | "STATUS": "0.0", 48 | "STATUS_ID": 101 49 | }, 50 | { 51 | "LABEL": "Huidige Amperage L2 (71.7.0)", 52 | "SECURITY": 0, 53 | "STATUS": "0.0", 54 | "STATUS_ID": 102 55 | }, 56 | { 57 | "LABEL": "Huidige Voltage L1 (32.7.0)", 58 | "SECURITY": 0, 59 | "STATUS": "229.0", 60 | "STATUS_ID": 103 61 | }, 62 | { 63 | "LABEL": "Huidige Voltage L2 (52.7.0)", 64 | "SECURITY": 0, 65 | "STATUS": "0.0", 66 | "STATUS_ID": 104 67 | }, 68 | { 69 | "LABEL": "Huidige Voltage L2 (72.7.0)", 70 | "SECURITY": 0, 71 | "STATUS": "0.0", 72 | "STATUS_ID": 105 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | """Exceptions tests for the P1Monitor device.""" 2 | 3 | # pylint: disable=protected-access 4 | import pytest 5 | from aresponses import ResponsesMockServer 6 | 7 | from p1monitor import P1Monitor 8 | from p1monitor.exceptions import P1MonitorConnectionError, P1MonitorError 9 | 10 | 11 | @pytest.mark.parametrize("status", [401, 403]) 12 | async def test_http_error401( 13 | aresponses: ResponsesMockServer, 14 | p1monitor_client: P1Monitor, 15 | status: int, 16 | ) -> None: 17 | """Test HTTP 401 response handling.""" 18 | aresponses.add( 19 | "192.168.1.2", 20 | "/api/v1/smartmeter", 21 | "GET", 22 | aresponses.Response(text="Give me energy!", status=status), 23 | ) 24 | with pytest.raises(P1MonitorConnectionError): 25 | assert await p1monitor_client._request("test") 26 | 27 | 28 | async def test_http_error404( 29 | aresponses: ResponsesMockServer, 30 | p1monitor_client: P1Monitor, 31 | ) -> None: 32 | """Test HTTP 404 response handling.""" 33 | aresponses.add( 34 | "192.168.1.2", 35 | "/api/v1/smartmeter", 36 | "GET", 37 | aresponses.Response(text="Give me energy!", status=404), 38 | ) 39 | with pytest.raises(P1MonitorError): 40 | assert await p1monitor_client._request("test") 41 | 42 | 43 | async def test_http_error500( 44 | aresponses: ResponsesMockServer, 45 | p1monitor_client: P1Monitor, 46 | ) -> None: 47 | """Test HTTP 500 response handling.""" 48 | aresponses.add( 49 | "192.168.1.2", 50 | "/api/v1/smartmeter", 51 | "GET", 52 | aresponses.Response( 53 | body=b'{"status":"nok"}', 54 | status=500, 55 | ), 56 | ) 57 | with pytest.raises(P1MonitorError): 58 | assert await p1monitor_client._request("test") 59 | 60 | 61 | async def test_no_success( 62 | aresponses: ResponsesMockServer, 63 | p1monitor_client: P1Monitor, 64 | ) -> None: 65 | """Test a message without a success message throws.""" 66 | aresponses.add( 67 | "192.168.1.2", 68 | "/api/v1/smartmeter", 69 | "GET", 70 | aresponses.Response( 71 | status=200, 72 | text='{"message": "no success"}', 73 | ), 74 | ) 75 | with pytest.raises(P1MonitorError): 76 | assert await p1monitor_client._request("test") 77 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | 120 | # Visual Studio Code 121 | .vscode 122 | 123 | # ruff 124 | .ruff_cache 125 | 126 | # Pyre type checker 127 | .pyre/ 128 | 129 | setup.py 130 | .python-version 131 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | 4 | # yamllint disable rule:line-length 5 | # yamllint disable-line rule:truthy 6 | on: 7 | release: 8 | types: 9 | - published 10 | 11 | env: 12 | DEFAULT_PYTHON: "3.12" 13 | 14 | jobs: 15 | release: 16 | name: Releasing to PyPi 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - name: ⤵️ Check out code from GitHub 23 | uses: actions/checkout@v6.0.1 24 | - name: 🏗 Set up Poetry 25 | run: pipx install poetry 26 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 27 | id: python 28 | uses: actions/setup-python@v6.1.0 29 | with: 30 | python-version: ${{ env.DEFAULT_PYTHON }} 31 | cache: "poetry" 32 | - name: 🏗 Install workflow dependencies 33 | run: | 34 | poetry config virtualenvs.create true 35 | poetry config virtualenvs.in-project true 36 | - name: 🏗 Install dependencies 37 | run: poetry install --no-interaction 38 | - name: 🏗 Set package version 39 | run: | 40 | version="${{ github.event.release.tag_name }}" 41 | version="${version,,}" 42 | version="${version#v}" 43 | poetry version --no-interaction "${version}" 44 | - name: 🏗 Build package 45 | run: poetry build --no-interaction 46 | - name: 🚀 Publish package to PyPi 47 | uses: pypa/gh-action-pypi-publish@v1.13.0 48 | 49 | tweet: 50 | name: 🐦 Tweet the release 51 | needs: release 52 | runs-on: ubuntu-latest 53 | permissions: 54 | contents: read 55 | steps: 56 | - uses: Eomm/why-don-t-you-tweet@v2.0.0 57 | with: 58 | # GitHub event payload 59 | # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#release 60 | tweet-message: | 61 | ⬆️ ${{ github.event.release.tag_name }} of ${{ github.event.repository.name }} just released 🎉 #update @klaasnicolaas #python #package #P1monitor #p1 #ztatz #release #bot #assistant 62 | 63 | Check out the release notes here: ${{ github.event.release.html_url }} 64 | env: 65 | # Get your tokens from https://developer.twitter.com/apps 66 | TWITTER_CONSUMER_API_KEY: ${{ secrets.TWITTER_CONSUMER_API_KEY }} 67 | TWITTER_CONSUMER_API_SECRET: ${{ secrets.TWITTER_CONSUMER_API_SECRET }} 68 | TWITTER_ACCESS_TOKEN: ${{ secrets.TWITTER_ACCESS_TOKEN }} 69 | TWITTER_ACCESS_TOKEN_SECRET: ${{ secrets.TWITTER_ACCESS_TOKEN_SECRET }} 70 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Testing 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | DEFAULT_PYTHON: "3.12" 12 | 13 | jobs: 14 | pytest: 15 | name: Python ${{ matrix.python }} 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python: ["3.12", "3.13", "3.14"] 20 | permissions: 21 | contents: read 22 | actions: read 23 | steps: 24 | - name: ⤵️ Check out code from GitHub 25 | uses: actions/checkout@v6.0.1 26 | - name: 🏗 Set up Poetry 27 | run: pipx install poetry 28 | - name: 🏗 Set up Python ${{ matrix.python }} 29 | id: python 30 | uses: actions/setup-python@v6.1.0 31 | with: 32 | python-version: ${{ matrix.python }} 33 | cache: "poetry" 34 | - name: 🏗 Install workflow dependencies 35 | run: | 36 | poetry config virtualenvs.create true 37 | poetry config virtualenvs.in-project true 38 | - name: 🏗 Install dependencies 39 | run: poetry install --no-interaction 40 | - name: 🚀 Run pytest 41 | run: poetry run pytest --cov src tests 42 | - name: ⬆️ Upload coverage artifact 43 | uses: actions/upload-artifact@v6.0.0 44 | with: 45 | name: coverage-${{ matrix.python }} 46 | include-hidden-files: true 47 | path: .coverage 48 | 49 | coverage: 50 | runs-on: ubuntu-latest 51 | needs: pytest 52 | permissions: 53 | contents: read 54 | actions: read 55 | steps: 56 | - name: ⤵️ Check out code from GitHub 57 | uses: actions/checkout@v6.0.1 58 | with: 59 | fetch-depth: 0 60 | - name: ⬇️ Download coverage data 61 | uses: actions/download-artifact@v7.0.0 62 | - name: 🏗 Set up Poetry 63 | run: pipx install poetry 64 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 65 | id: python 66 | uses: actions/setup-python@v6.1.0 67 | with: 68 | python-version: ${{ env.DEFAULT_PYTHON }} 69 | cache: 'poetry' 70 | - name: 🏗 Install workflow dependencies 71 | run: | 72 | poetry config virtualenvs.create true 73 | poetry config virtualenvs.in-project true 74 | - name: 🏗 Install dependencies 75 | run: poetry install --no-interaction 76 | - name: 🚀 Process coverage results 77 | run: | 78 | poetry run coverage combine coverage*/.coverage* 79 | poetry run coverage xml -i 80 | - name: 🚀 Upload coverage report 81 | uses: codecov/codecov-action@v5.5.2 82 | with: 83 | token: ${{ secrets.CODECOV_TOKEN }} 84 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "${containerWorkspaceFolderBasename}", 3 | "image": "mcr.microsoft.com/devcontainers/python:3.14", 4 | "features": { 5 | "ghcr.io/devcontainers/features/git:1": {}, 6 | "ghcr.io/devcontainers/features/github-cli:1": {}, 7 | "ghcr.io/devcontainers/features/python:1": { 8 | "installTools": false 9 | }, 10 | "ghcr.io/devcontainers/features/common-utils:2": { 11 | "installOhMyZsh": true 12 | }, 13 | "ghcr.io/devcontainers-extra/features/poetry:2": {}, 14 | "ghcr.io/devcontainers-extra/features/pre-commit:2": {}, 15 | "ghcr.io/devcontainers-extra/features/zsh-plugins:0": { 16 | "plugins": "git zsh-autosuggestions zsh-syntax-highlighting zsh-completions", 17 | "omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions.git https://github.com/zsh-users/zsh-syntax-highlighting.git https://github.com/zsh-users/zsh-completions.git" 18 | }, 19 | "ghcr.io/hwaien/devcontainer-features/match-host-time-zone": {} 20 | }, 21 | "customizations": { 22 | "vscode": { 23 | "extensions": [ 24 | "GitHub.copilot", 25 | "GitHub.vscode-github-actions", 26 | "GitHub.vscode-pull-request-github", 27 | "Tyriar.sort-lines", 28 | "charliermarsh.ruff", 29 | "esbenp.prettier-vscode", 30 | "mhutchie.git-graph", 31 | "ms-python.python", 32 | "oderwat.indent-rainbow", 33 | "redhat.vscode-yaml", 34 | "ryanluker.vscode-coverage-gutters" 35 | ], 36 | "settings": { 37 | "[python]": { 38 | "editor.defaultFormatter": "charliermarsh.ruff", 39 | "editor.codeActionsOnSave": { 40 | "source.fixAll": true, 41 | "source.organizeImports": true 42 | } 43 | }, 44 | "ruff.importStrategy": "fromEnvironment", 45 | "ruff.interpreter": [".venv/bin/python"], 46 | "python.analysis.extraPaths": ["${workspaceFolder}/src"], 47 | "python.defaultInterpreterPath": ".venv/bin/python", 48 | "python.formatting.provider": "ruff", 49 | "python.linting.enabled": true, 50 | "python.linting.mypyEnabled": true, 51 | "python.linting.pylintEnabled": false, 52 | "python.testing.cwd": "${workspaceFolder}", 53 | "python.testing.pytestEnabled": true, 54 | "python.testing.pytestArgs": ["--cov-report=xml"], 55 | "coverage-gutters.customizable.context-menu": true, 56 | "coverage-gutters.customizable.status-bar-toggler-watchCoverageAndVisibleEditors-enabled": true, 57 | "coverage-gutters.showGutterCoverage": false, 58 | "coverage-gutters.showLineCoverage": true, 59 | "coverage-gutters.xmlname": "coverage.xml", 60 | "terminal.integrated.defaultProfile.linux": "zsh" 61 | } 62 | } 63 | }, 64 | "postCreateCommand": "poetry config virtualenvs.in-project true && poetry install", 65 | "onCreateCommand": "", 66 | "remoteUser": "vscode" 67 | } 68 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "breaking-change" 3 | color: d93f0b 4 | description: "A breaking change for existing users." 5 | - name: "bug" 6 | color: fc2929 7 | description: "Inconsistencies or issues which will cause a problem for users." 8 | - name: "bugfix" 9 | color: ededed 10 | description: "Fixing a bug." 11 | - name: "documentation" 12 | color: 0052cc 13 | description: "Solely about the documentation of the project." 14 | - name: "enhancement" 15 | color: 1d76db 16 | description: "Enhancement of the code, not introducing new features." 17 | - name: "refactor" 18 | color: 1d76db 19 | description: "Improvement of existing code, not introducing new features." 20 | - name: "performance" 21 | color: 1d76db 22 | description: "Improving performance, not introducing new features." 23 | - name: "new-feature" 24 | color: 0e8a16 25 | description: "New features or request." 26 | - name: "maintenance" 27 | color: 2af79e 28 | description: "Generic maintenance tasks." 29 | - name: "ci" 30 | color: 1d76db 31 | description: "Work that improves the continue integration." 32 | - name: "dependencies" 33 | color: 1d76db 34 | description: "Upgrade or downgrade of project dependencies." 35 | 36 | - name: "in-progress" 37 | color: fbca04 38 | description: "Issue is currently being resolved by a developer." 39 | - name: "stale" 40 | color: fef2c0 41 | description: "There has not been activity on this issue or PR for quite some time." 42 | - name: "no-stale" 43 | color: fef2c0 44 | description: "This issue or PR is exempted from the stable bot." 45 | - name: "wontfix" 46 | color: ffffff 47 | description: "This issue or PR will not be fixed." 48 | - name: "cleanup" 49 | color: ef75d5 50 | description: "Cleanup of code." 51 | - name: "sync" 52 | color: 00a6ed 53 | description: "Syncing with upstream github config repository." 54 | 55 | - name: "security" 56 | color: ee0701 57 | description: "Marks a security issue that needs to be resolved asap." 58 | - name: "incomplete" 59 | color: fef2c0 60 | description: "Marks a PR or issue that is missing information." 61 | - name: "invalid" 62 | color: fef2c0 63 | description: "Marks a PR or issue that is missing information." 64 | - name: "duplicate" 65 | color: cfd3d7 66 | description: "This issue or pull request already exists." 67 | 68 | - name: "beginner-friendly" 69 | color: 0e8a16 70 | description: "Good first issue for people wanting to contribute to the project." 71 | - name: "help-wanted" 72 | color: 0e8a16 73 | description: "We need some extra helping hands or expertise in order to resolve this." 74 | 75 | - name: "hacktoberfest" 76 | description: "Issues/PRs are participating in the Hacktoberfest." 77 | color: fbca04 78 | - name: "hacktoberfest-accepted" 79 | description: "Issues/PRs are participating in the Hacktoberfest." 80 | color: fbca04 81 | 82 | - name: "priority-critical" 83 | color: ee0701 84 | description: "This should be dealt with ASAP. Not fixing this issue would be a serious error." 85 | - name: "priority-high" 86 | color: b60205 87 | description: "After critical issues are fixed, these should be dealt with before any further issues." 88 | - name: "priority-medium" 89 | color: 0e8a16 90 | description: "This issue may be useful, and needs some attention." 91 | - name: "priority-low" 92 | color: e4ea8a 93 | description: "Nice addition, maybe... someday..." 94 | 95 | - name: "major" 96 | color: b60205 97 | description: "This PR causes a major version bump in the version number." 98 | - name: "minor" 99 | color: 0e8a16 100 | description: "This PR causes a minor version bump in the version number." 101 | -------------------------------------------------------------------------------- /tests/test_p1monitor.py: -------------------------------------------------------------------------------- 1 | """Basic tests for the P1Monitor device.""" 2 | 3 | # pylint: disable=protected-access 4 | import asyncio 5 | from unittest.mock import patch 6 | 7 | import pytest 8 | from aiohttp import ClientError, ClientResponse, ClientSession 9 | from aresponses import Response, ResponsesMockServer 10 | 11 | from p1monitor import P1Monitor 12 | from p1monitor.exceptions import P1MonitorConnectionError, P1MonitorError 13 | 14 | from . import load_fixtures 15 | 16 | 17 | async def test_json_request( 18 | aresponses: ResponsesMockServer, 19 | p1monitor_client: P1Monitor, 20 | ) -> None: 21 | """Test JSON response is handled correctly.""" 22 | aresponses.add( 23 | "192.168.1.2", 24 | "/api/test", 25 | "GET", 26 | aresponses.Response( 27 | status=200, 28 | headers={"Content-Type": "application/json"}, 29 | text='{"status": "ok"}', 30 | ), 31 | ) 32 | response = await p1monitor_client._request("test") 33 | assert response is not None 34 | await p1monitor_client.close() 35 | 36 | 37 | async def test_different_port(aresponses: ResponsesMockServer) -> None: 38 | """Test different port is handled correctly.""" 39 | aresponses.add( 40 | "192.168.1.2:84", 41 | "/api/test", 42 | "GET", 43 | aresponses.Response( 44 | status=200, 45 | headers={"Content-Type": "application/json"}, 46 | text='{"status": "ok"}', 47 | ), 48 | ) 49 | async with ClientSession() as session: 50 | p1monitor = P1Monitor("192.168.1.2", port=84, session=session) 51 | await p1monitor._request("test") 52 | await p1monitor.close() 53 | 54 | 55 | async def test_internal_session(aresponses: ResponsesMockServer) -> None: 56 | """Test JSON response is handled correctly.""" 57 | aresponses.add( 58 | "192.168.1.2", 59 | "/api/test", 60 | "GET", 61 | aresponses.Response( 62 | status=200, 63 | headers={"Content-Type": "application/json"}, 64 | text='{"status": "ok"}', 65 | ), 66 | ) 67 | async with P1Monitor("192.168.1.2") as p1monitor: 68 | await p1monitor._request("test") 69 | 70 | 71 | async def test_timeout(aresponses: ResponsesMockServer) -> None: 72 | """Test request timeout from P1 Monitor.""" 73 | 74 | # Faking a timeout by sleeping 75 | async def reponse_handler(_: ClientResponse) -> Response: 76 | await asyncio.sleep(0.2) 77 | return aresponses.Response( 78 | body="Goodmorning!", 79 | text=load_fixtures("smartmeter.json"), 80 | ) 81 | 82 | aresponses.add("192.168.1.2", "/api/test", "GET", reponse_handler) 83 | 84 | async with ClientSession() as session: 85 | client = P1Monitor(host="192.168.1.2", session=session, request_timeout=0.1) 86 | with pytest.raises(P1MonitorConnectionError): 87 | assert await client._request("test") 88 | 89 | 90 | async def test_content_type( 91 | aresponses: ResponsesMockServer, 92 | p1monitor_client: P1Monitor, 93 | ) -> None: 94 | """Test request content type error from P1 Monitor.""" 95 | aresponses.add( 96 | "192.168.1.2", 97 | "/api/test", 98 | "GET", 99 | aresponses.Response( 100 | status=200, 101 | headers={"Content-Type": "blabla/blabla"}, 102 | ), 103 | ) 104 | with pytest.raises(P1MonitorError): 105 | assert await p1monitor_client._request("test") 106 | 107 | 108 | async def test_client_error() -> None: 109 | """Test request client error from P1 Monitor.""" 110 | async with ClientSession() as session: 111 | client = P1Monitor(host="192.168.1.2", session=session) 112 | with ( 113 | patch.object( 114 | session, 115 | "request", 116 | side_effect=ClientError, 117 | ), 118 | pytest.raises(P1MonitorConnectionError), 119 | ): 120 | assert await client._request("test") 121 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: local 4 | hooks: 5 | - id: ruff-check 6 | name: 🐶 Ruff Linter 7 | language: system 8 | types: [python] 9 | entry: poetry run ruff check --fix 10 | require_serial: true 11 | stages: [pre-commit, pre-push, manual] 12 | - id: ruff-format 13 | name: 🐶 Ruff Formatter 14 | language: system 15 | types: [python] 16 | entry: poetry run ruff format 17 | require_serial: true 18 | stages: [pre-commit, pre-push, manual] 19 | - id: check-ast 20 | name: 🐍 Check Python AST 21 | language: system 22 | types: [python] 23 | entry: poetry run check-ast 24 | - id: check-case-conflict 25 | name: 🔠 Check for case conflicts 26 | language: system 27 | entry: poetry run check-case-conflict 28 | - id: check-docstring-first 29 | name: ℹ️ Check docstring is first 30 | language: system 31 | types: [python] 32 | entry: poetry run check-docstring-first 33 | - id: check-executables-have-shebangs 34 | name: 🧐 Check that executables have shebangs 35 | language: system 36 | types: [text, executable] 37 | entry: poetry run check-executables-have-shebangs 38 | stages: [pre-commit, pre-push, manual] 39 | - id: check-json 40 | name: { Check JSON files 41 | language: system 42 | types: [json] 43 | entry: poetry run check-json 44 | - id: check-merge-conflict 45 | name: 💥 Check for merge conflicts 46 | language: system 47 | types: [text] 48 | entry: poetry run check-merge-conflict 49 | - id: check-symlinks 50 | name: 🔗 Check for broken symlinks 51 | language: system 52 | types: [symlink] 53 | entry: poetry run check-symlinks 54 | - id: check-toml 55 | name: ✅ Check TOML files 56 | language: system 57 | types: [toml] 58 | entry: poetry run check-toml 59 | - id: check-xml 60 | name: ✅ Check XML files 61 | entry: poetry run check-xml 62 | language: system 63 | types: [xml] 64 | - id: check-yaml 65 | name: ✅ Check YAML files 66 | language: system 67 | types: [yaml] 68 | entry: poetry run check-yaml 69 | - id: codespell 70 | name: ✅ Check code for common misspellings 71 | language: system 72 | types: [text] 73 | exclude: ^poetry\.lock$ 74 | entry: poetry run codespell 75 | - id: detect-private-key 76 | name: 🕵️ Detect Private Keys 77 | language: system 78 | types: [text] 79 | entry: poetry run detect-private-key 80 | - id: end-of-file-fixer 81 | name: ⮐ Fix End of Files 82 | language: system 83 | types: [text] 84 | entry: poetry run end-of-file-fixer 85 | stages: [pre-commit, pre-push, manual] 86 | - id: mypy 87 | name: 🆎 Static type checking using mypy 88 | language: system 89 | types: [python] 90 | entry: poetry run mypy 91 | - id: no-commit-to-branch 92 | name: 🛑 Don't commit to main branch 93 | language: system 94 | entry: poetry run no-commit-to-branch 95 | pass_filenames: false 96 | always_run: true 97 | args: 98 | - --branch=main 99 | - id: poetry 100 | name: 📜 Check pyproject with Poetry 101 | language: system 102 | entry: poetry check 103 | pass_filenames: false 104 | always_run: true 105 | - id: pylint 106 | name: 🌟 Starring code with pylint 107 | language: system 108 | types: [python] 109 | entry: poetry run pylint 110 | - id: pytest 111 | name: 🧪 Running tests and test coverage with pytest 112 | language: system 113 | types: [python] 114 | entry: poetry run pytest 115 | pass_filenames: false 116 | - id: trailing-whitespace 117 | name: ✄ Trim Trailing Whitespace 118 | language: system 119 | types: [text] 120 | entry: poetry run trailing-whitespace-fixer 121 | stages: [pre-commit, pre-push, manual] 122 | - id: yamllint 123 | name: 🎗 Check YAML files with yamllint 124 | language: system 125 | types: [yaml] 126 | entry: poetry run yamllint 127 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | """Test the models.""" 2 | 3 | import pytest 4 | from aiohttp import ClientSession 5 | from aresponses import ResponsesMockServer 6 | from syrupy.assertion import SnapshotAssertion 7 | 8 | from p1monitor import ( 9 | P1Monitor, 10 | P1MonitorConnectionError, 11 | P1MonitorNoDataError, 12 | Phases, 13 | Settings, 14 | SmartMeter, 15 | WaterMeter, 16 | ) 17 | 18 | from . import load_fixtures 19 | 20 | 21 | async def test_smartmeter( 22 | aresponses: ResponsesMockServer, 23 | snapshot: SnapshotAssertion, 24 | p1monitor_client: P1Monitor, 25 | ) -> None: 26 | """Test request from a P1 Monitor device - SmartMeter object.""" 27 | aresponses.add( 28 | "192.168.1.2", 29 | "/api/v1/smartmeter", 30 | "GET", 31 | aresponses.Response( 32 | text=load_fixtures("smartmeter.json"), 33 | status=200, 34 | headers={"Content-Type": "application/json; charset=utf-8"}, 35 | ), 36 | ) 37 | smartmeter: SmartMeter = await p1monitor_client.smartmeter() 38 | assert smartmeter == snapshot 39 | 40 | 41 | async def test_phases( 42 | aresponses: ResponsesMockServer, 43 | snapshot: SnapshotAssertion, 44 | p1monitor_client: P1Monitor, 45 | ) -> None: 46 | """Test request from a P1 Monitor device - Phases object.""" 47 | aresponses.add( 48 | "192.168.1.2", 49 | "/api/v1/status", 50 | "GET", 51 | aresponses.Response( 52 | text=load_fixtures("phases.json"), 53 | status=200, 54 | headers={"Content-Type": "application/json; charset=utf-8"}, 55 | ), 56 | ) 57 | phases: Phases = await p1monitor_client.phases() 58 | assert phases == snapshot 59 | 60 | 61 | async def test_watermeter( 62 | aresponses: ResponsesMockServer, 63 | snapshot: SnapshotAssertion, 64 | p1monitor_client: P1Monitor, 65 | ) -> None: 66 | """Test request from a P1 Monitor device - WaterMeter object.""" 67 | aresponses.add( 68 | "192.168.1.2", 69 | "/api/v2/watermeter/day", 70 | "GET", 71 | aresponses.Response( 72 | text=load_fixtures("watermeter.json"), 73 | status=200, 74 | headers={"Content-Type": "application/json; charset=utf-8"}, 75 | ), 76 | ) 77 | watermeter: WaterMeter = await p1monitor_client.watermeter() 78 | assert watermeter == snapshot 79 | 80 | 81 | async def test_no_watermeter_data_new(aresponses: ResponsesMockServer) -> None: 82 | """Test no WaterMeter data from P1 Monitor device.""" 83 | aresponses.add( 84 | "192.168.1.2", 85 | "/api/v2/watermeter/day", 86 | "GET", 87 | aresponses.Response( 88 | text=load_fixtures("no_data.json"), 89 | status=200, 90 | headers={"Content-Type": "application/json; charset=utf-8"}, 91 | ), 92 | ) 93 | 94 | async with ClientSession() as session: 95 | client = P1Monitor(host="192.168.1.2", session=session) 96 | with pytest.raises(P1MonitorNoDataError): 97 | await client.watermeter() 98 | 99 | 100 | async def test_no_watermeter_data_old(aresponses: ResponsesMockServer) -> None: 101 | """Test no WaterMeter data from P1 Monitor device.""" 102 | aresponses.add( 103 | "192.168.1.2", 104 | "/api/v2/watermeter/day", 105 | "GET", 106 | aresponses.Response( 107 | status=404, 108 | headers={"Content-Type": "application/json; charset=utf-8"}, 109 | ), 110 | ) 111 | 112 | async with ClientSession() as session: 113 | client = P1Monitor(host="192.168.1.2", session=session) 114 | with pytest.raises(P1MonitorConnectionError): 115 | await client.watermeter() 116 | 117 | 118 | async def test_settings( 119 | aresponses: ResponsesMockServer, 120 | snapshot: SnapshotAssertion, 121 | p1monitor_client: P1Monitor, 122 | ) -> None: 123 | """Test request from a P1 Monitor device - Settings object.""" 124 | aresponses.add( 125 | "192.168.1.2", 126 | "/api/v1/configuration", 127 | "GET", 128 | aresponses.Response( 129 | text=load_fixtures("settings.json"), 130 | status=200, 131 | headers={"Content-Type": "application/json; charset=utf-8"}, 132 | ), 133 | ) 134 | settings: Settings = await p1monitor_client.settings() 135 | assert settings == snapshot 136 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "p1monitor" 3 | version = "0.0.0" 4 | description = "Asynchronous Python client for the P1 Monitor" 5 | authors = [{ name="Klaas Schoute", email="<hello@student-techlife.com>"}] 6 | maintainers = [{name="Klaas Schoute", email="<hello@student-techlife.com>"}] 7 | license = "MIT" 8 | requires-python = ">=3.12" 9 | readme = "README.md" 10 | keywords = ["p1", "monitor", "power", "energy", "api", "async", "client"] 11 | classifiers = [ 12 | "Development Status :: 5 - Production/Stable", 13 | "Framework :: AsyncIO", 14 | "Intended Audience :: Developers", 15 | "License :: OSI Approved :: MIT License", 16 | "Natural Language :: English", 17 | "Programming Language :: Python :: 3.12", 18 | "Programming Language :: Python :: 3.13", 19 | "Programming Language :: Python :: 3.14", 20 | "Programming Language :: Python :: 3", 21 | "Topic :: Software Development :: Libraries :: Python Modules", 22 | ] 23 | dynamic = ["dependencies"] 24 | packages = [ 25 | { include = "p1monitor", from = "src"}, 26 | ] 27 | 28 | [tool.poetry.dependencies] 29 | aiohttp = ">=3.0.0" 30 | python = "^3.12" 31 | yarl = ">=1.6.0" 32 | 33 | [project.urls] 34 | homepage = "https://github.com/klaasnicolaas/python-p1monitor" 35 | repository = "https://github.com/klaasnicolaas/python-p1monitor" 36 | documentation = "https://github.com/klaasnicolaas/python-p1monitor" 37 | "Bug Tracker" = "https://github.com/klaasnicolaas/python-p1monitor/issues" 38 | Changelog = "https://github.com/klaasnicolaas/python-p1monitor/releases" 39 | 40 | [tool.poetry.group.dev.dependencies] 41 | aresponses = "3.0.0" 42 | codespell = "2.4.1" 43 | covdefaults = "2.3.0" 44 | coverage = {version = "7.13.0", extras = ["toml"]} 45 | mypy = "1.19.1" 46 | pre-commit = "4.5.1" 47 | pre-commit-hooks = "6.0.0" 48 | pylint = "4.0.4" 49 | pytest = "9.0.2" 50 | pytest-asyncio = "1.3.0" 51 | pytest-cov = "7.0.0" 52 | ruff = "0.14.10" 53 | syrupy = "5.0.0" 54 | yamllint = "1.37.1" 55 | 56 | [tool.coverage.run] 57 | plugins = ["covdefaults"] 58 | source = ["p1monitor"] 59 | 60 | [tool.coverage.report] 61 | fail_under = 90 62 | show_missing = true 63 | 64 | [tool.mypy] 65 | # Specify the target platform details in config, so your developers are 66 | # free to run mypy on Windows, Linux, or macOS and get consistent 67 | # results. 68 | platform = "linux" 69 | python_version = "3.12" 70 | 71 | # flake8-mypy expects the two following for sensible formatting 72 | show_column_numbers = true 73 | 74 | # show error messages from unrelated files 75 | follow_imports = "normal" 76 | 77 | # suppress errors about unsatisfied imports 78 | ignore_missing_imports = true 79 | 80 | # be strict 81 | check_untyped_defs = true 82 | disallow_any_generics = true 83 | disallow_incomplete_defs = true 84 | disallow_subclassing_any = true 85 | disallow_untyped_calls = true 86 | disallow_untyped_decorators = true 87 | disallow_untyped_defs = true 88 | no_implicit_optional = true 89 | no_implicit_reexport = true 90 | strict_optional = true 91 | warn_incomplete_stub = true 92 | warn_no_return = true 93 | warn_redundant_casts = true 94 | warn_return_any = true 95 | warn_unused_configs = true 96 | warn_unused_ignores = true 97 | 98 | [tool.pylint.BASIC] 99 | good-names = ["_", "ex", "fp", "i", "id", "j", "k", "on", "Run", "T"] 100 | 101 | [tool.pylint."MESSAGES CONTROL"] 102 | disable= [ 103 | "duplicate-code", 104 | "format", 105 | "unsubscriptable-object", 106 | ] 107 | 108 | [tool.pylint.SIMILARITIES] 109 | ignore-imports = true 110 | 111 | [tool.pylint.FORMAT] 112 | max-line-length = 88 113 | 114 | [tool.pylint.DESIGN] 115 | max-attributes = 15 116 | 117 | [tool.pytest.ini_options] 118 | addopts = "--cov" 119 | asyncio_mode = "auto" 120 | 121 | [tool.ruff] 122 | target-version = "py312" 123 | lint.select = ["ALL"] 124 | lint.ignore = [ 125 | "ANN401", # Opinionated warning on disallowing dynamically typed expressions 126 | "D203", # Conflicts with other rules 127 | "D213", # Conflicts with other rules 128 | "D417", # False positives in some occasions 129 | "PLR2004", # Just annoying, not really useful 130 | "SLOT000", # Has a bug with enums: https://github.com/astral-sh/ruff/issues/5748 131 | 132 | # Conflicts with the Ruff formatter 133 | "COM812", 134 | ] 135 | 136 | 137 | [tool.ruff.lint.flake8-pytest-style] 138 | mark-parentheses = false 139 | fixture-parentheses = false 140 | 141 | [tool.ruff.lint.isort] 142 | known-first-party = ["p1monitor"] 143 | 144 | [tool.ruff.lint.mccabe] 145 | max-complexity = 25 146 | 147 | [build-system] 148 | build-backend = "poetry.core.masonry.api" 149 | requires = ["poetry-core>=1.0.0"] 150 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socioeconomic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | hello@student-techlife.com. 64 | 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][faq]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [mozilla coc]: https://github.com/mozilla/diversity 132 | [faq]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /.github/workflows/linting.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Linting 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | DEFAULT_PYTHON: "3.12" 12 | 13 | jobs: 14 | codespell: 15 | name: codespell 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | steps: 20 | - name: ⤵️ Check out code from GitHub 21 | uses: actions/checkout@v6.0.1 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@v6.1.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 Python dependencies 35 | run: poetry install --no-interaction 36 | - name: 🚀 Check code for common misspellings 37 | run: poetry run pre-commit run codespell --all-files 38 | 39 | ruff: 40 | name: Ruff 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | steps: 45 | - name: ⤵️ Check out code from GitHub 46 | uses: actions/checkout@v6.0.1 47 | - name: 🏗 Set up Poetry 48 | run: pipx install poetry 49 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 50 | id: python 51 | uses: actions/setup-python@v6.1.0 52 | with: 53 | python-version: ${{ env.DEFAULT_PYTHON }} 54 | cache: "poetry" 55 | - name: 🏗 Install workflow dependencies 56 | run: | 57 | poetry config virtualenvs.create true 58 | poetry config virtualenvs.in-project true 59 | - name: 🏗 Install Python dependencies 60 | run: poetry install --no-interaction 61 | - name: 🚀 Run Ruff linter 62 | run: poetry run ruff check --output-format=github . 63 | - name: 🚀 Run Ruff formatter 64 | run: poetry run ruff format --check . 65 | 66 | pre-commit-hooks: 67 | name: pre-commit-hooks 68 | runs-on: ubuntu-latest 69 | permissions: 70 | contents: read 71 | steps: 72 | - name: ⤵️ Check out code from GitHub 73 | uses: actions/checkout@v6.0.1 74 | - name: 🏗 Set up Poetry 75 | run: pipx install poetry 76 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 77 | id: python 78 | uses: actions/setup-python@v6.1.0 79 | with: 80 | python-version: ${{ env.DEFAULT_PYTHON }} 81 | cache: "poetry" 82 | - name: 🏗 Install workflow dependencies 83 | run: | 84 | poetry config virtualenvs.create true 85 | poetry config virtualenvs.in-project true 86 | - name: 🏗 Install Python dependencies 87 | run: poetry install --no-interaction 88 | - name: 🚀 Check Python AST 89 | run: poetry run pre-commit run check-ast --all-files 90 | - name: 🚀 Check for case conflicts 91 | run: poetry run pre-commit run check-case-conflict --all-files 92 | - name: 🚀 Check docstring is first 93 | run: poetry run pre-commit run check-docstring-first --all-files 94 | - name: 🚀 Check that executables have shebangs 95 | run: poetry run pre-commit run check-executables-have-shebangs --all-files 96 | - name: 🚀 Check JSON files 97 | run: poetry run pre-commit run check-json --all-files 98 | - name: 🚀 Check for merge conflicts 99 | run: poetry run pre-commit run check-merge-conflict --all-files 100 | - name: 🚀 Check for broken symlinks 101 | run: poetry run pre-commit run check-symlinks --all-files 102 | - name: 🚀 Check TOML files 103 | run: poetry run pre-commit run check-toml --all-files 104 | - name: 🚀 Check XML files 105 | run: poetry run pre-commit run check-xml --all-files 106 | - name: 🚀 Check YAML files 107 | run: poetry run pre-commit run check-yaml --all-files 108 | - name: 🚀 Detect Private Keys 109 | run: poetry run pre-commit run detect-private-key --all-files 110 | - name: 🚀 Check End of Files 111 | run: poetry run pre-commit run end-of-file-fixer --all-files 112 | - name: 🚀 Trim Trailing Whitespace 113 | run: poetry run pre-commit run trailing-whitespace --all-files 114 | 115 | pylint: 116 | name: pylint 117 | runs-on: ubuntu-latest 118 | permissions: 119 | contents: read 120 | steps: 121 | - name: ⤵️ Check out code from GitHub 122 | uses: actions/checkout@v6.0.1 123 | - name: 🏗 Set up Poetry 124 | run: pipx install poetry 125 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 126 | id: python 127 | uses: actions/setup-python@v6.1.0 128 | with: 129 | python-version: ${{ env.DEFAULT_PYTHON }} 130 | cache: "poetry" 131 | - name: 🏗 Install workflow dependencies 132 | run: | 133 | poetry config virtualenvs.create true 134 | poetry config virtualenvs.in-project true 135 | - name: 🏗 Install Python dependencies 136 | run: poetry install --no-interaction 137 | - name: 🚀 Run pylint 138 | run: poetry run pre-commit run pylint --all-files 139 | 140 | yamllint: 141 | name: yamllint 142 | runs-on: ubuntu-latest 143 | permissions: 144 | contents: read 145 | steps: 146 | - name: ⤵️ Check out code from GitHub 147 | uses: actions/checkout@v6.0.1 148 | - name: 🏗 Set up Poetry 149 | run: pipx install poetry 150 | - name: 🏗 Set up Python ${{ env.DEFAULT_PYTHON }} 151 | id: python 152 | uses: actions/setup-python@v6.1.0 153 | with: 154 | python-version: ${{ env.DEFAULT_PYTHON }} 155 | cache: "poetry" 156 | - name: 🏗 Install workflow dependencies 157 | run: | 158 | poetry config virtualenvs.create true 159 | poetry config virtualenvs.in-project true 160 | - name: 🏗 Install Python dependencies 161 | run: poetry install --no-interaction 162 | - name: 🚀 Run yamllint 163 | run: poetry run yamllint . 164 | -------------------------------------------------------------------------------- /src/p1monitor/p1monitor.py: -------------------------------------------------------------------------------- 1 | """Asynchronous Python client for the P1 Monitor API.""" 2 | 3 | from __future__ import annotations 4 | 5 | import asyncio 6 | import socket 7 | from dataclasses import dataclass 8 | from importlib import metadata 9 | from typing import Any, Self 10 | 11 | from aiohttp import ClientError, ClientSession 12 | from aiohttp.hdrs import METH_GET 13 | from yarl import URL 14 | 15 | from .exceptions import P1MonitorConnectionError, P1MonitorError, P1MonitorNoDataError 16 | from .models import Phases, Settings, SmartMeter, WaterMeter 17 | 18 | VERSION = metadata.version(__package__) 19 | 20 | 21 | @dataclass 22 | class P1Monitor: 23 | """Main class for handling connections with the P1 Monitor API.""" 24 | 25 | host: str 26 | port: int = 80 27 | request_timeout: float = 10.0 28 | session: ClientSession | None = None 29 | 30 | _close_session: bool = False 31 | 32 | async def _request( 33 | self, 34 | uri: str, 35 | *, 36 | method: str = METH_GET, 37 | params: dict[str, Any] | None = None, 38 | ) -> Any: 39 | """Handle a request to a P1 Monitor device. 40 | 41 | Args: 42 | ---- 43 | uri: Request URI, without '/api/', for example, 'status' 44 | method: HTTP Method to use. 45 | params: Extra options to improve or limit the response. 46 | 47 | Returns: 48 | ------- 49 | A Python dictionary (JSON decoded) with the response from 50 | the P1 Monitor API. 51 | 52 | Raises: 53 | ------ 54 | P1MonitorConnectionError: An error occurred while communicating 55 | with the P1 Monitor. 56 | P1MonitorError: Received an unexpected response from the P1 Monitor API. 57 | 58 | """ 59 | url = URL.build( 60 | scheme="http", 61 | host=self.host, 62 | port=int(self.port), 63 | path="/api/", 64 | ).join(URL(uri)) 65 | 66 | headers = { 67 | "User-Agent": f"PythonP1Monitor/{VERSION}", 68 | "Accept": "application/json, text/plain, */*", 69 | } 70 | 71 | if self.session is None: 72 | self.session = ClientSession() 73 | self._close_session = True 74 | 75 | try: 76 | async with asyncio.timeout(self.request_timeout): 77 | response = await self.session.request( 78 | method, 79 | url, 80 | params=params, 81 | headers=headers, 82 | ) 83 | response.raise_for_status() 84 | 85 | except TimeoutError as exception: 86 | msg = "Timeout occurred while connecting to P1 Monitor device" 87 | raise P1MonitorConnectionError( 88 | msg, 89 | ) from exception 90 | except (ClientError, socket.gaierror) as exception: 91 | if "watermeter" in uri and response.status == 404: 92 | msg = "No water meter is connected to P1 Monitor device" 93 | raise P1MonitorConnectionError(msg) from exception 94 | msg = "Error occurred while communicating with P1 Monitor device" 95 | raise P1MonitorConnectionError(msg) from exception 96 | 97 | content_type = response.headers.get("Content-Type", "") 98 | if "application/json" not in content_type: 99 | text = await response.text() 100 | msg = "Unexpected response from the P1 Monitor device" 101 | raise P1MonitorError( 102 | msg, 103 | {"Content-Type": content_type, "response": text}, 104 | ) 105 | 106 | return await response.json() 107 | 108 | async def smartmeter(self) -> SmartMeter: 109 | """Get the latest values from you smart meter. 110 | 111 | Returns 112 | ------- 113 | A SmartMeter data object from the P1 Monitor API. 114 | 115 | """ 116 | data = await self._request( 117 | "v1/smartmeter", 118 | params={"json": "object", "limit": 1}, 119 | ) 120 | return SmartMeter.from_dict(data) 121 | 122 | async def settings(self) -> Settings: 123 | """Receive the set price values for energy and gas. 124 | 125 | Returns 126 | ------- 127 | A Settings data object from the P1 Monitor API. 128 | 129 | """ 130 | data = await self._request("v1/configuration", params={"json": "object"}) 131 | return Settings.from_dict(data) 132 | 133 | async def phases(self) -> Phases: 134 | """Receive data from all phases on your smart meter. 135 | 136 | Returns 137 | ------- 138 | A Phases data object from the P1 Monitor API. 139 | 140 | """ 141 | data = await self._request("v1/status", params={"json": "object"}) 142 | return Phases.from_dict(data) 143 | 144 | async def watermeter(self) -> WaterMeter: 145 | """Get the latest values from you water meter. 146 | 147 | Returns 148 | ------- 149 | A WaterMeter data object from the P1 Monitor API. 150 | 151 | Raises 152 | ------ 153 | P1MonitorNoDataError: No data was received from the P1 Monitor API. 154 | 155 | """ 156 | data = await self._request( 157 | "v2/watermeter/day", 158 | params={"json": "object", "limit": 1}, 159 | ) 160 | if data == []: 161 | msg = "No data received from P1 Monitor" 162 | raise P1MonitorNoDataError(msg) 163 | return WaterMeter.from_dict(data) 164 | 165 | async def close(self) -> None: 166 | """Close open client session.""" 167 | if self.session and self._close_session: 168 | await self.session.close() 169 | 170 | async def __aenter__(self) -> Self: 171 | """Async enter. 172 | 173 | Returns 174 | ------- 175 | The P1 Monitor object. 176 | 177 | """ 178 | return self 179 | 180 | async def __aexit__(self, *_exc_info: object) -> None: 181 | """Async exit. 182 | 183 | Args: 184 | ---- 185 | _exc_info: Exec type. 186 | 187 | """ 188 | await self.close() 189 | -------------------------------------------------------------------------------- /src/p1monitor/models.py: -------------------------------------------------------------------------------- 1 | """Models for P1 Monitor.""" 2 | 3 | from __future__ import annotations 4 | 5 | from dataclasses import dataclass 6 | from enum import Enum 7 | from typing import Any 8 | 9 | 10 | class EnergyTariff(str, Enum): 11 | """Enumeration representing the rate period.""" 12 | 13 | __slots__ = () 14 | 15 | LOW = "low" 16 | HIGH = "high" 17 | 18 | 19 | @dataclass 20 | class SmartMeter: 21 | """Object representing an SmartMeter response from P1 Monitor.""" 22 | 23 | gas_consumption: float | None 24 | energy_tariff_period: str | None 25 | 26 | power_consumption: int | None 27 | energy_consumption_high: float | None 28 | energy_consumption_low: float | None 29 | 30 | power_production: int | None 31 | energy_production_high: float | None 32 | energy_production_low: float | None 33 | 34 | @staticmethod 35 | def from_dict(data: dict[str | int, Any]) -> SmartMeter: 36 | """Return SmartMeter object from the P1 Monitor API response. 37 | 38 | Args: 39 | ---- 40 | data: The data from the P1 Monitor API. 41 | 42 | Returns: 43 | ------- 44 | A SmartMeter object. 45 | 46 | """ 47 | 48 | def energy_tariff(tariff: str) -> EnergyTariff: 49 | """Return API energy_tariff information. 50 | 51 | Args: 52 | ---- 53 | tariff: The provided tariff code from the API. 54 | 55 | Returns: 56 | ------- 57 | The energy tariff period class. 58 | 59 | """ 60 | if tariff == "P": 61 | return EnergyTariff.HIGH 62 | return EnergyTariff.LOW 63 | 64 | data = data[0] 65 | return SmartMeter( 66 | gas_consumption=data.get("CONSUMPTION_GAS_M3"), 67 | power_consumption=data.get("CONSUMPTION_W"), 68 | energy_consumption_high=data.get("CONSUMPTION_KWH_HIGH"), 69 | energy_consumption_low=data.get("CONSUMPTION_KWH_LOW"), 70 | power_production=data.get("PRODUCTION_W"), 71 | energy_production_high=data.get("PRODUCTION_KWH_HIGH"), 72 | energy_production_low=data.get("PRODUCTION_KWH_LOW"), 73 | energy_tariff_period=energy_tariff(str(data.get("TARIFCODE"))), 74 | ) 75 | 76 | 77 | @dataclass 78 | class Settings: 79 | """Object representing an Settings response from P1 Monitor.""" 80 | 81 | gas_consumption_price: float | None 82 | 83 | energy_consumption_price_high: float | None 84 | energy_consumption_price_low: float | None 85 | 86 | energy_production_price_high: float | None 87 | energy_production_price_low: float | None 88 | 89 | @staticmethod 90 | def from_dict(data: dict[str, Any]) -> Settings: 91 | """Return Settings object from the P1 Monitor API response. 92 | 93 | Args: 94 | ---- 95 | data: The data from the P1 Monitor API. 96 | 97 | Returns: 98 | ------- 99 | A Settings object. 100 | 101 | """ 102 | return Settings( 103 | gas_consumption_price=search(15, data, "conf"), 104 | energy_consumption_price_low=search(1, data, "conf"), 105 | energy_consumption_price_high=search(2, data, "conf"), 106 | energy_production_price_low=search(3, data, "conf"), 107 | energy_production_price_high=search(4, data, "conf"), 108 | ) 109 | 110 | 111 | @dataclass 112 | class Phases: 113 | """Object representing an Phases response from P1 Monitor.""" 114 | 115 | voltage_phase_l1: float | None 116 | voltage_phase_l2: float | None 117 | voltage_phase_l3: float | None 118 | 119 | current_phase_l1: float | None 120 | current_phase_l2: float | None 121 | current_phase_l3: float | None 122 | 123 | power_consumed_phase_l1: int | None 124 | power_consumed_phase_l2: int | None 125 | power_consumed_phase_l3: int | None 126 | 127 | power_produced_phase_l1: int | None 128 | power_produced_phase_l2: int | None 129 | power_produced_phase_l3: int | None 130 | 131 | @staticmethod 132 | def from_dict(data: dict[str | int, Any]) -> Phases: 133 | """Return Phases object from the P1 Monitor API response. 134 | 135 | Args: 136 | ---- 137 | data: The data from the P1 Monitor API. 138 | 139 | Returns: 140 | ------- 141 | A Phases object. 142 | 143 | """ 144 | return Phases( 145 | voltage_phase_l1=search(103, data, "status"), 146 | voltage_phase_l2=search(104, data, "status"), 147 | voltage_phase_l3=search(105, data, "status"), 148 | current_phase_l1=search(100, data, "status"), 149 | current_phase_l2=search(101, data, "status"), 150 | current_phase_l3=search(102, data, "status"), 151 | power_consumed_phase_l1=convert(search(74, data, "status")), 152 | power_consumed_phase_l2=convert(search(75, data, "status")), 153 | power_consumed_phase_l3=convert(search(76, data, "status")), 154 | power_produced_phase_l1=convert(search(77, data, "status")), 155 | power_produced_phase_l2=convert(search(78, data, "status")), 156 | power_produced_phase_l3=convert(search(79, data, "status")), 157 | ) 158 | 159 | 160 | @dataclass 161 | class WaterMeter: 162 | """Object representing an WaterMeter response from P1 Monitor.""" 163 | 164 | consumption_day: int | None 165 | consumption_total: float | None 166 | pulse_count: int | None 167 | 168 | @staticmethod 169 | def from_dict(data: dict[str | int, Any]) -> WaterMeter: 170 | """Return WaterMeter object from the P1 Monitor API response. 171 | 172 | Args: 173 | ---- 174 | data: The data from the P1 Monitor API. 175 | 176 | Returns: 177 | ------- 178 | A WaterMeter object. 179 | 180 | """ 181 | data = data[0] 182 | return WaterMeter( 183 | consumption_day=data.get("WATERMETER_CONSUMPTION_LITER"), 184 | consumption_total=data.get("WATERMETER_CONSUMPTION_TOTAL_M3"), 185 | pulse_count=data.get("WATERMETER_PULS_COUNT"), 186 | ) 187 | 188 | 189 | def search(position: int, data: Any, service: str) -> float: 190 | """Find the correct value in the json data file. 191 | 192 | Args: 193 | ---- 194 | position: The position ID number. 195 | data: The JSON list which is requested from the API. 196 | service: Type of dataclass. 197 | 198 | Returns: 199 | ------- 200 | The value that corresponds to the specified position. 201 | 202 | """ 203 | value: float 204 | for i in data: 205 | if service == "conf" and i["CONFIGURATION_ID"] == position: 206 | value = float(i["PARAMETER"]) 207 | elif service == "status" and i["STATUS_ID"] == position: 208 | value = float(i["STATUS"]) 209 | return value 210 | 211 | 212 | def convert(value: float) -> int: 213 | """Convert values from kW to W. 214 | 215 | Args: 216 | ---- 217 | value: The current value. 218 | 219 | Returns: 220 | ------- 221 | Value in Watt (W). 222 | 223 | """ 224 | return int(float(value) * 1000) 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | <!-- Banner --> 2 | ![alt Banner of the P1 Monitor package](https://raw.githubusercontent.com/klaasnicolaas/python-p1monitor/main/assets/header_p1monitor-min.png) 3 | 4 | <!-- PROJECT SHIELDS --> 5 | [![GitHub Release][releases-shield]][releases] 6 | [![Python Versions][python-versions-shield]][pypi] 7 | ![Project Stage][project-stage-shield] 8 | ![Project Maintenance][maintenance-shield] 9 | [![License][license-shield]](LICENSE) 10 | 11 | [![GitHub Activity][commits-shield]][commits-url] 12 | [![PyPi Downloads][downloads-shield]][downloads-url] 13 | [![GitHub Last Commit][last-commit-shield]][commits-url] 14 | [![Open in Dev Containers][devcontainer-shield]][devcontainer] 15 | 16 | [![Build Status][build-shield]][build-url] 17 | [![Typing Status][typing-shield]][typing-url] 18 | [![Code Coverage][codecov-shield]][codecov-url] 19 | 20 | Asynchronous Python client for the P1 Monitor API. 21 | 22 | ## About 23 | 24 | There are many ways to read the serial port (P1) of your smart meter and what you do with the data that comes out. With this python library your platform can read [P1 Monitor][p1-monitor] via the API and use the data for example for an integration in [Home Assistant][home-assistant]. 25 | 26 | ## Installation 27 | 28 | ```bash 29 | pip install p1monitor 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```python 35 | import asyncio 36 | 37 | from p1monitor import P1Monitor 38 | 39 | 40 | async def main(): 41 | """Show example on getting P1 Monitor data.""" 42 | async with P1Monitor(host="192.168.1.2", port=80) as client: 43 | smartmeter = await client.smartmeter() 44 | watermeter = await client.watermeter() 45 | settings = await client.settings() 46 | phases = await client.phases() 47 | print(smartmeter) 48 | print(watermeter) 49 | print(settings) 50 | print(phases) 51 | 52 | 53 | if __name__ == "__main__": 54 | asyncio.run(main()) 55 | ``` 56 | 57 | More examples can be found in the [examples folder](./examples/). 58 | 59 | ## Class: `P1Monitor` 60 | 61 | This is the main class that you will use to interact with the P1 Monitor. 62 | 63 | | Parameter | Required | Description | 64 | | --------- | -------- | -------------------------------------------- | 65 | | `host` | `True` | The IP address of the P1 Monitor. | 66 | | `port` | `False` | The port of the P1 Monitor. Default is `80`. | 67 | 68 | ## Data 69 | 70 | There is a lot of data that you can read via the API: 71 | 72 | ### SmartMeter 73 | 74 | - Gas Consumption 75 | - Power Consumption / Production 76 | - Energy Consumption Low/High 77 | - Energy Production Low/High 78 | - Energy Tariff Period 79 | 80 | ### Phases 81 | 82 | - Voltage phases L1/2/3 83 | - Current Phases L1/2/3 84 | - Power consumed phases L1/2/3 85 | - Power Produced phases L1/2/3 86 | 87 | ### WaterMeter 88 | 89 | > [!IMPORTANT] 90 | > WaterMeter is only available when you run version 1.1.0 or higher due the use of the new v2 API url. 91 | 92 | - Day Consumption (liters) 93 | - Total Consumption (m3) 94 | - Day Pulse count 95 | 96 | ### Settings 97 | 98 | - Gas Consumption Price 99 | - Energy Consumption Price Low/High 100 | - Energy Production Price Low/High 101 | 102 | ## Contributing 103 | 104 | This is an active open-source project. We are always open to people who want to 105 | use the code or contribute to it. 106 | 107 | We've set up a separate document for our 108 | [contribution guidelines](CONTRIBUTING.md). 109 | 110 | Thank you for being involved! :heart_eyes: 111 | 112 | ## Setting up development environment 113 | 114 | The simplest way to begin is by utilizing the [Dev Container][devcontainer] 115 | feature of Visual Studio Code or by opening a CodeSpace directly on GitHub. 116 | By clicking the button below you immediately start a Dev Container in Visual Studio Code. 117 | 118 | [![Open in Dev Containers][devcontainer-shield]][devcontainer] 119 | 120 | This Python project relies on [Poetry][poetry] as its dependency manager, 121 | providing comprehensive management and control over project dependencies. 122 | 123 | You need at least: 124 | 125 | - Python 3.12+ 126 | - [Poetry][poetry-install] 127 | 128 | ### Installation 129 | 130 | Install all packages, including all development requirements: 131 | 132 | ```bash 133 | poetry install 134 | ``` 135 | 136 | _Poetry creates by default an virtual environment where it installs all 137 | necessary pip packages_. 138 | 139 | ### Pre-commit 140 | 141 | This repository uses the [pre-commit][pre-commit] framework, all changes 142 | are linted and tested with each commit. To setup the pre-commit check, run: 143 | 144 | ```bash 145 | poetry run pre-commit install 146 | ``` 147 | 148 | And to run all checks and tests manually, use the following command: 149 | 150 | ```bash 151 | poetry run pre-commit run --all-files 152 | ``` 153 | 154 | ### Testing 155 | 156 | It uses [pytest](https://docs.pytest.org/en/stable/) as the test framework. To run the tests: 157 | 158 | ```bash 159 | poetry run pytest 160 | ``` 161 | 162 | To update the [syrupy](https://github.com/tophat/syrupy) snapshot tests: 163 | 164 | ```bash 165 | poetry run pytest --snapshot-update 166 | ``` 167 | 168 | ## License 169 | 170 | MIT License 171 | 172 | Copyright (c) 2021-2025 Klaas Schoute 173 | 174 | Permission is hereby granted, free of charge, to any person obtaining a copy 175 | of this software and associated documentation files (the "Software"), to deal 176 | in the Software without restriction, including without limitation the rights 177 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 178 | copies of the Software, and to permit persons to whom the Software is 179 | furnished to do so, subject to the following conditions: 180 | 181 | The above copyright notice and this permission notice shall be included in all 182 | copies or substantial portions of the Software. 183 | 184 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 185 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 186 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 187 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 188 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 189 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 190 | SOFTWARE. 191 | 192 | <!-- MARKDOWN LINKS & IMAGES --> 193 | [build-shield]: https://github.com/klaasnicolaas/python-p1monitor/actions/workflows/tests.yaml/badge.svg 194 | [build-url]: https://github.com/klaasnicolaas/python-p1monitor/actions/workflows/tests.yaml 195 | [commits-shield]: https://img.shields.io/github/commit-activity/y/klaasnicolaas/python-p1monitor.svg 196 | [commits-url]: https://github.com/klaasnicolaas/python-p1monitor/commits/main 197 | [codecov-shield]: https://codecov.io/gh/klaasnicolaas/python-p1monitor/branch/main/graph/badge.svg?token=G4FIVHJVZR 198 | [codecov-url]: https://codecov.io/gh/klaasnicolaas/python-p1monitor 199 | [devcontainer-shield]: https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode 200 | [devcontainer]: https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/klaasnicolaas/python-p1monitor 201 | [downloads-shield]: https://img.shields.io/pypi/dm/p1monitor 202 | [downloads-url]: https://pypistats.org/packages/p1monitor 203 | [license-shield]: https://img.shields.io/github/license/klaasnicolaas/python-p1monitor.svg 204 | [last-commit-shield]: https://img.shields.io/github/last-commit/klaasnicolaas/python-p1monitor.svg 205 | [maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg 206 | [project-stage-shield]: https://img.shields.io/badge/project%20stage-production%20ready-brightgreen.svg 207 | [pypi]: https://pypi.org/project/p1monitor/ 208 | [python-versions-shield]: https://img.shields.io/pypi/pyversions/p1monitor 209 | [typing-shield]: https://github.com/klaasnicolaas/python-p1monitor/actions/workflows/typing.yaml/badge.svg 210 | [typing-url]: https://github.com/klaasnicolaas/python-p1monitor/actions/workflows/typing.yaml 211 | [releases-shield]: https://img.shields.io/github/release/klaasnicolaas/python-p1monitor.svg 212 | [releases]: https://github.com/klaasnicolaas/python-p1monitor/releases 213 | 214 | [p1-monitor]: https://www.ztatz.nl/p1-monitor 215 | [home-assistant]: https://www.home-assistant.io 216 | [poetry-install]: https://python-poetry.org/docs/#installation 217 | [poetry]: https://python-poetry.org 218 | [pre-commit]: https://pre-commit.com 219 | --------------------------------------------------------------------------------