├── .gitattributes ├── requirements_dev.txt ├── requirements.txt ├── requirements_test.txt ├── .gitignore ├── images ├── logo.png ├── appareil.png ├── daily_consumption.png ├── monthly_consumption.png └── apexcharts-card_example.png ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── hacs.json ├── .devcontainer ├── configuration.yaml ├── devcontainer.json └── README.md ├── custom_components └── veolia │ ├── translations │ ├── en.json │ └── fr.json │ ├── manifest.json │ ├── debug.py │ ├── const.py │ ├── entity.py │ ├── config_flow.py │ ├── __init__.py │ ├── sensor.py │ └── VeoliaClient.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── issue.md ├── dependabot.yml └── workflows │ ├── cron.yaml │ └── push-pull.yaml ├── LICENSE ├── setup.cfg ├── CHANGELOG.md ├── README.md └── info.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | homeassistant 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.31.0 2 | xmltodict==0.13.0 -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | pytest-homeassistant-custom-component==0.13.168 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | pythonenv* 3 | venv 4 | .venv 5 | .coverage 6 | .idea 7 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kugan49/veolia-custom-component-HACS/HEAD/images/logo.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "ms-python.python"] 3 | } 4 | -------------------------------------------------------------------------------- /images/appareil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kugan49/veolia-custom-component-HACS/HEAD/images/appareil.png -------------------------------------------------------------------------------- /images/daily_consumption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kugan49/veolia-custom-component-HACS/HEAD/images/daily_consumption.png -------------------------------------------------------------------------------- /images/monthly_consumption.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kugan49/veolia-custom-component-HACS/HEAD/images/monthly_consumption.png -------------------------------------------------------------------------------- /images/apexcharts-card_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kugan49/veolia-custom-component-HACS/HEAD/images/apexcharts-card_example.png -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Veolia", 3 | "hacs": "1.20.0", 4 | "render_readme": true, 5 | "homeassistant": "2022.11.0", 6 | "country": "FR" 7 | } 8 | -------------------------------------------------------------------------------- /.devcontainer/configuration.yaml: -------------------------------------------------------------------------------- 1 | default_config: 2 | 3 | logger: 4 | default: info 5 | logs: 6 | custom_components.veolia: debug 7 | 8 | # If you need to debug uncomment the line below (doc: https://www.home-assistant.io/integrations/debugpy/) 9 | debugpy: 10 | -------------------------------------------------------------------------------- /custom_components/veolia/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Veolia by Kugan49", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "description": "Please use your https://www.service.eau.veolia.fr/ credentials.", 7 | "data": { 8 | "username": "Username", 9 | "password": "Password", 10 | "abo_id": "Your Subscription Reference" 11 | } 12 | } 13 | }, 14 | "error": { 15 | "auth": "Username/Password is wrong." 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/veolia/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "veolia", 3 | "name": "Veolia", 4 | "codeowners": [ 5 | "@kugan49" 6 | ], 7 | "config_flow": true, 8 | "dependencies": [], 9 | "documentation": "https://github.com/kugan49/veolia-custom-component-HACS", 10 | "integration_type": "device", 11 | "iot_class": "cloud_polling", 12 | "issue_tracker": "https://github.com/kugan49/veolia-custom-component-HACS/issues", 13 | "requirements": [ 14 | "xmltodict==0.13.0" 15 | ], 16 | "version": "1.3.1" 17 | } -------------------------------------------------------------------------------- /custom_components/veolia/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Veolia by Kugan49", 3 | "config": { 4 | "step": { 5 | "user": { 6 | "description": "Utiliser vos identifiants pour https://www.service.eau.veolia.fr/", 7 | "data": { 8 | "username": "Identifiant", 9 | "password": "Mot de Passe", 10 | "abo_id": "Votre Référence Abonnement" 11 | } 12 | } 13 | }, 14 | "error": { 15 | "auth": "Identifiant ou mot de passe erroné." 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /custom_components/veolia/debug.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | _LOGGER = logging.getLogger(__name__) 4 | 5 | 6 | def decoratorexceptionDebug(f): 7 | def decoratorfunction(*args, **kwargs): 8 | result = None 9 | try: 10 | _LOGGER.debug(f"Start function {f.__name__}") 11 | result = f(*args, **kwargs) 12 | _LOGGER.debug(f"End function {f.__name__}") 13 | except Exception as e: 14 | _LOGGER.error(f"Error in function {f.__name__}: {e}") 15 | return result 16 | 17 | return decoratorfunction 18 | -------------------------------------------------------------------------------- /custom_components/veolia/const.py: -------------------------------------------------------------------------------- 1 | """Constants for Veolia.""" 2 | # Base component constants 3 | NAME = "Veolia" 4 | DOMAIN = "veolia" 5 | ATTRIBUTION = "Data provided by https://www.service.eau.veolia.fr/" 6 | 7 | 8 | # Icons 9 | ICON = "mdi:water" 10 | 11 | # Platforms 12 | SENSOR = "sensor" 13 | PLATFORMS = [SENSOR] 14 | 15 | # Configuration and options 16 | CONF_USERNAME = "username" 17 | CONF_PASSWORD = "password" 18 | CONF_ABO_ID = "abo_id" 19 | 20 | # API = "api" 21 | DAILY = "daily" 22 | MONTHLY = "monthly" 23 | HISTORY = "historyConsumption" 24 | FORMAT_DATE = "%Y-%m-%dT%H:%M:%S%z" 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | version: 2 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | 12 | - package-ecosystem: "pip" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | ignore: 17 | # Dependabot should not update Home Assistant as that should match the homeassistant key in hacs.json 18 | - dependency-name: "homeassistant" 19 | -------------------------------------------------------------------------------- /.github/workflows/cron.yaml: -------------------------------------------------------------------------------- 1 | name: Cron actions 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | jobs: 8 | hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest 9 | name: "Hassfest Validation" 10 | runs-on: "ubuntu-latest" 11 | steps: 12 | - name: "Checkout the repository" 13 | uses: "actions/checkout@v4" 14 | - name: "Run hassfest validation" 15 | uses: "home-assistant/actions/hassfest@master" 16 | hacs: # https://github.com/hacs/action 17 | name: "HACS Validation" 18 | runs-on: "ubuntu-latest" 19 | steps: 20 | - name: "Checkout the repository" 21 | uses: "actions/checkout@v4" 22 | - name: "Run HACS validation" 23 | uses: "hacs/action@main" 24 | with: 25 | category: "integration" 26 | ignore: "brands" 27 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Run Home Assistant on port 9123", 6 | "type": "shell", 7 | "command": "container start", 8 | "problemMatcher": [] 9 | }, 10 | { 11 | "label": "Run Home Assistant configuration against /config", 12 | "type": "shell", 13 | "command": "container check", 14 | "problemMatcher": [] 15 | }, 16 | { 17 | "label": "Upgrade Home Assistant to latest dev", 18 | "type": "shell", 19 | "command": "container install", 20 | "problemMatcher": [] 21 | }, 22 | { 23 | "label": "Install a specific version of Home Assistant", 24 | "type": "shell", 25 | "command": "container set-version", 26 | "problemMatcher": [] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": false, 3 | "editor.formatOnSave": true, 4 | "python.formatting.provider": "black", 5 | "python.formatting.blackArgs": ["--line-length", "120"], 6 | "python.linting.lintOnSave": true, 7 | "python.linting.enabled": true, 8 | "python.linting.pylintEnabled": false, 9 | "python.linting.pylintPath": ".venv\\Scripts\\pylint", 10 | "python.linting.pylintArgs": [ 11 | "--load-plugins", 12 | "--enable=W0614", 13 | "--enable= W0611" 14 | ], 15 | "[python]": { 16 | "editor.tabSize": 4, 17 | "editor.codeActionsOnSave": { 18 | "source.organizeImports": "explicit" 19 | } 20 | }, 21 | "files.exclude": { 22 | ".venv": true, 23 | "**/__pycache__": true 24 | }, 25 | "python.linting.flake8Enabled": true, 26 | "python.analysis.typeCheckingMode": "off", 27 | "todo-tree.tree.scanMode": "workspace", 28 | "files.associations": { 29 | "*.yaml": "home-assistant" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details. 2 | { 3 | "image": "ghcr.io/ludeeus/devcontainer/integration:stable", 4 | "name": "Blueprint integration development", 5 | "context": "..", 6 | "appPort": [ 7 | "9123:8123" 8 | ], 9 | "postCreateCommand": "container install", 10 | "extensions": [ 11 | "ms-python.python", 12 | "github.vscode-pull-request-github", 13 | "ryanluker.vscode-coverage-gutters", 14 | "ms-python.vscode-pylance" 15 | ], 16 | "settings": { 17 | "files.eol": "\n", 18 | "editor.tabSize": 4, 19 | "terminal.integrated.shell.linux": "/bin/bash", 20 | "python.pythonPath": "/usr/bin/python3", 21 | "python.analysis.autoSearchPaths": false, 22 | "python.linting.pylintEnabled": true, 23 | "python.linting.enabled": true, 24 | "python.formatting.provider": "black", 25 | "editor.formatOnPaste": false, 26 | "editor.formatOnSave": true, 27 | "editor.formatOnType": true, 28 | "files.trimTrailingWhitespace": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 16 | 17 | ## Version of the custom_component 18 | 21 | 22 | ## Configuration 23 | 24 | ```yaml 25 | 26 | Add your logs here. 27 | 28 | ``` 29 | 30 | ## Describe the bug 31 | A clear and concise description of what the bug is. 32 | 33 | 34 | ## Debug log 35 | 36 | 37 | 38 | ```text 39 | 40 | Add your logs here. 41 | 42 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Joakim Sørensen @ludeeus 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. -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build 3 | doctests = True 4 | # To work with Black 5 | max-line-length = 88 6 | # E501: line too long 7 | # W503: Line break occurred before a binary operator 8 | # E203: Whitespace before ':' 9 | # D202 No blank lines allowed after function docstring 10 | # W504 line break after binary operator 11 | ignore = 12 | E501, 13 | W503, 14 | E203, 15 | D202, 16 | W504 17 | 18 | [isort] 19 | # https://github.com/timothycrosley/isort 20 | # https://github.com/timothycrosley/isort/wiki/isort-Settings 21 | # splits long import on multiple lines indented by 4 spaces 22 | multi_line_output = 3 23 | include_trailing_comma=True 24 | force_grid_wrap=0 25 | use_parentheses=True 26 | line_length=88 27 | indent = " " 28 | # by default isort don't check module indexes 29 | not_skip = __init__.py 30 | # will group `import x` and `from x import` of the same module. 31 | force_sort_within_sections = true 32 | sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 33 | default_section = THIRDPARTY 34 | known_first_party = custom_components.integration_blueprint, tests 35 | combine_as_imports = true 36 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Python : fichier actif", 7 | "type": "python", 8 | "request": "launch", 9 | "program": "${file}", 10 | "console": "integratedTerminal", 11 | "justMyCode": true 12 | }, 13 | { 14 | // Example of attaching to local debug server 15 | "name": "Python: Attach Local", 16 | "type": "python", 17 | "request": "attach", 18 | "port": 5678, 19 | "host": "localhost", 20 | "pathMappings": [ 21 | { 22 | "localRoot": "${workspaceFolder}", 23 | "remoteRoot": "." 24 | } 25 | ] 26 | }, 27 | { 28 | // Example of attaching to my production server 29 | "name": "Python: Attach Remote", 30 | "type": "python", 31 | "request": "attach", 32 | "port": 5678, 33 | "host": "homeassistant.local", 34 | "pathMappings": [ 35 | { 36 | "localRoot": "${workspaceFolder}", 37 | "remoteRoot": "/usr/src/homeassistant" 38 | } 39 | ] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/push-pull.yaml: -------------------------------------------------------------------------------- 1 | name: Checks and release 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | 8 | jobs: 9 | hassfest: # https://developers.home-assistant.io/blog/2020/04/16/hassfest 10 | name: "Hassfest Validation" 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - name: "Checkout the repository" 14 | uses: "actions/checkout@v4" 15 | - name: "Run hassfest validation" 16 | uses: "home-assistant/actions/hassfest@master" 17 | hacs: # https://github.com/hacs/action 18 | name: "HACS Validation" 19 | runs-on: "ubuntu-latest" 20 | steps: 21 | - name: "Checkout the repository" 22 | uses: "actions/checkout@v4" 23 | - name: "Run HACS validation" 24 | uses: "hacs/action@main" 25 | with: 26 | category: "integration" 27 | ignore: "brands" 28 | style: 29 | runs-on: "ubuntu-latest" 30 | name: Check style formatting 31 | steps: 32 | - uses: "actions/checkout@v4" 33 | - uses: "actions/setup-python@v5" 34 | with: 35 | python-version: "3.x" 36 | - run: python3 -m pip install black 37 | - run: black . 38 | release: 39 | needs: [hassfest, hacs, style] 40 | runs-on: "ubuntu-latest" 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: CupOfTea696/gh-action-auto-release@v1.0.2 44 | with: 45 | title: "Release: $version" 46 | tag: "v$semver" 47 | draft: false 48 | regex: "/^Release: #{semver}$/i" 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /custom_components/veolia/entity.py: -------------------------------------------------------------------------------- 1 | """VeoliaEntity class.""" 2 | 3 | from homeassistant.components.sensor import SensorDeviceClass, SensorEntity 4 | from homeassistant.const import UnitOfVolume 5 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 6 | 7 | from .const import ATTRIBUTION, DAILY, DOMAIN, HISTORY, ICON, NAME 8 | from .debug import decoratorexceptionDebug 9 | 10 | 11 | class VeoliaEntity(CoordinatorEntity, SensorEntity): 12 | """Representation of a Veolia entity.""" 13 | 14 | @decoratorexceptionDebug 15 | def __init__(self, coordinator, config_entry): 16 | """Initialize the entity.""" 17 | super().__init__(coordinator) 18 | self.config_entry = config_entry 19 | 20 | @property 21 | def unique_id(self): 22 | """Return a unique ID to use for this entity.""" 23 | return f"{self.config_entry.entry_id}_{self.name}" 24 | # return self.config_entry.entry_id 25 | 26 | @property 27 | @decoratorexceptionDebug 28 | def device_info(self): 29 | """Return device registry information for this entity.""" 30 | return { 31 | "identifiers": {(DOMAIN, self.config_entry.entry_id)}, 32 | "manufacturer": NAME, 33 | "name": NAME, 34 | } 35 | 36 | @property 37 | def device_class(self): 38 | """Return the device_class of the sensor.""" 39 | return SensorDeviceClass.WATER 40 | 41 | @property 42 | def unit_of_measurement(self): 43 | """Return the unit_of_measurement of the sensor.""" 44 | return UnitOfVolume.LITERS 45 | 46 | @property 47 | def icon(self): 48 | """Return the icon of the sensor.""" 49 | return ICON 50 | 51 | def _base_extra_state_attributes(self): 52 | """Return the base extra state attributes.""" 53 | return { 54 | "attribution": ATTRIBUTION, 55 | "integration": DOMAIN, 56 | "last_report": self.coordinator.data[DAILY][HISTORY][0][0], 57 | } 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## v1.3.1 6 | 7 | ### Changed 8 | 9 | - Mise à jour des dépendances 10 | - Mise à jour fonctions dépréciées par home assistant 11 | 12 | ## v1.3.0 13 | 14 | ### Changed 15 | 16 | - Mise à jour des dépendances 17 | - Ajout info tableau de bord energie dans Readme 18 | 19 | 20 | ## v1.2.0 21 | 22 | ### Breaking change 23 | 24 | --- 25 | **La configuration perd le mot de pase en passant à cette version.** 26 | 27 | **Il faut supprimer l'intégration PUIS la recréer, mais les historiques de données seront repris.** 28 | 29 | --- 30 | 31 | ### Changed 32 | 33 | - Passage de la doc en Français, l'usage étant réservé à la France. 34 | - Ajout de la gestion multi compteur. 35 | Dans la configuration, vous pourrez préciser la référence d'abonnement (voir Readme) 36 | Vous devez pour cela supprimer l'intégration actuelle pour la recréer. 37 | 38 | 39 | ## v1.1.2 40 | 41 | ### Changed 42 | 43 | - Améliorer le retour si erreur 500 : possibilité de récupérer le message du serveur 44 | 45 | ## v1.1.1 46 | 47 | ### Changed 48 | 49 | - Ajout d'un controle si l'historique des consos ne ramène qu'un résultat 50 | 51 | ## v1.1.0 52 | 53 | ### Breaking change 54 | 55 | - le format d'historique a été modifié pour être plus facilement intégrable dans apexcharts-card 56 | 57 | ### Changed 58 | 59 | - Ajout d'un exemple d'utilisation d'apexcharts-card pour l'historique 60 | 61 | ## v1.0.3 62 | 63 | ### Changed 64 | 65 | - Ajout de traces 66 | 67 | ## v1.0.2 68 | 69 | ### Changed 70 | 71 | - Gestion de l'abonnement si plusieurs abonnements 72 | 73 | ## v1.0.1 74 | 75 | ### Changed 76 | 77 | - Amélioration des traces 78 | 79 | ## v1.0.0 80 | 81 | ### Changed 82 | 83 | - reformatage du code 84 | - `veolia_last_index` peut être utilisé dans le tableau de bord `Energie` 85 | 86 | ## v0.0.4 87 | 88 | ### Changed 89 | 90 | - Correction bug sur last_index 91 | 92 | ## v0.0.3 93 | 94 | ### Changed 95 | 96 | - Added sensor last_index compatible with energy dashboard 97 | 98 | ## v0.0.2 99 | 100 | ### Changed 101 | 102 | - Added sensor monthly_consumption 103 | 104 | ## v0.0.1 105 | 106 | - First Beta unreleased 107 | -------------------------------------------------------------------------------- /custom_components/veolia/config_flow.py: -------------------------------------------------------------------------------- 1 | """Adds config flow for Veolia.""" 2 | import logging 3 | 4 | from homeassistant import config_entries 5 | import voluptuous as vol 6 | 7 | from .VeoliaClient import BadCredentialsException, VeoliaClient 8 | from .const import CONF_ABO_ID, CONF_PASSWORD, CONF_USERNAME, DOMAIN 9 | from .debug import decoratorexceptionDebug 10 | 11 | _LOGGER: logging.Logger = logging.getLogger(__package__) 12 | 13 | 14 | class VeoliaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 15 | """Config flow for veolia.""" 16 | 17 | VERSION = 1 18 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL 19 | 20 | @decoratorexceptionDebug 21 | def __init__(self): 22 | """Initialize.""" 23 | self._errors = {} 24 | 25 | @decoratorexceptionDebug 26 | async def async_step_user(self, user_input=None): 27 | """Handle a flow initialized by the user.""" 28 | self._errors = {} 29 | 30 | # Uncomment the next 2 lines if only a single instance of the integration is allowed: 31 | # if self._async_current_entries(): 32 | # return self.async_abort(reason="single_instance_allowed") 33 | 34 | if user_input is not None: 35 | valid = await self._test_credentials(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) 36 | if valid: 37 | if user_input[CONF_ABO_ID] != "": 38 | title = f"{user_input[CONF_USERNAME]} - {user_input[CONF_ABO_ID]}" 39 | else: 40 | title = f"{user_input[CONF_USERNAME]}" 41 | return self.async_create_entry(title=title, data=user_input) 42 | else: 43 | self._errors["base"] = "auth" 44 | 45 | return await self._show_config_form(user_input) 46 | 47 | user_input = {} 48 | # Provide defaults for form 49 | user_input[CONF_USERNAME] = "" 50 | user_input[CONF_PASSWORD] = "" 51 | user_input[CONF_ABO_ID] = "" 52 | 53 | return await self._show_config_form(user_input) 54 | 55 | @decoratorexceptionDebug 56 | async def _show_config_form(self, user_input): 57 | """Show the configuration form to edit location data.""" 58 | return self.async_show_form( 59 | step_id="user", 60 | data_schema=vol.Schema( 61 | { 62 | vol.Required(CONF_USERNAME, default=user_input[CONF_USERNAME]): str, 63 | vol.Required(CONF_PASSWORD, default=user_input[CONF_PASSWORD]): str, 64 | vol.Optional(CONF_ABO_ID, default=user_input[CONF_ABO_ID]): str, 65 | } 66 | ), 67 | errors=self._errors, 68 | ) 69 | 70 | @decoratorexceptionDebug 71 | async def _test_credentials(self, username, password): 72 | """Return true if credentials is valid.""" 73 | try: 74 | client = VeoliaClient(username, password) 75 | await self.hass.async_add_executor_job(client.login) 76 | return True 77 | except BadCredentialsException: 78 | pass 79 | return False 80 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | ## Developing with Visual Studio Code + devcontainer 2 | 3 | The easiest way to get started with custom integration development is to use Visual Studio Code with devcontainers. This approach will create a preconfigured development environment with all the tools you need. 4 | 5 | In the container you will have a dedicated Home Assistant core instance running with your custom component code. You can configure this instance by updating the `./devcontainer/configuration.yaml` file. 6 | 7 | **Prerequisites** 8 | 9 | - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 10 | - Docker 11 | - For Linux, macOS, or Windows 10 Pro/Enterprise/Education use the [current release version of Docker](https://docs.docker.com/install/) 12 | - Windows 10 Home requires [WSL 2](https://docs.microsoft.com/windows/wsl/wsl2-install) and the current Edge version of Docker Desktop (see instructions [here](https://docs.docker.com/docker-for-windows/wsl-tech-preview/)). This can also be used for Windows Pro/Enterprise/Education. 13 | - [Visual Studio code](https://code.visualstudio.com/) 14 | - [Remote - Containers (VSC Extension)][extension-link] 15 | 16 | [More info about requirements and devcontainer in general](https://code.visualstudio.com/docs/remote/containers#_getting-started) 17 | 18 | [extension-link]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers 19 | 20 | **Getting started:** 21 | 22 | 1. Fork the repository. 23 | 2. Clone the repository to your computer. 24 | 3. Open the repository using Visual Studio code. 25 | 26 | When you open this repository with Visual Studio code you are asked to "Reopen in Container", this will start the build of the container. 27 | 28 | _If you don't see this notification, open the command palette and select `Remote-Containers: Reopen Folder in Container`._ 29 | 30 | ### Tasks 31 | 32 | The devcontainer comes with some useful tasks to help you with development, you can start these tasks by opening the command palette and select `Tasks: Run Task` then select the task you want to run. 33 | 34 | When a task is currently running (like `Run Home Assistant on port 9123` for the docs), it can be restarted by opening the command palette and selecting `Tasks: Restart Running Task`, then select the task you want to restart. 35 | 36 | The available tasks are: 37 | 38 | Task | Description 39 | -- | -- 40 | Run Home Assistant on port 9123 | Launch Home Assistant with your custom component code and the configuration defined in `.devcontainer/configuration.yaml`. 41 | Run Home Assistant configuration against /config | Check the configuration. 42 | Upgrade Home Assistant to latest dev | Upgrade the Home Assistant core version in the container to the latest version of the `dev` branch. 43 | Install a specific version of Home Assistant | Install a specific version of Home Assistant core in the container. 44 | 45 | ### Step by Step debugging 46 | 47 | With the development container, 48 | you can test your custom component in Home Assistant with step by step debugging. 49 | 50 | You need to modify the `configuration.yaml` file in `.devcontainer` folder 51 | by uncommenting the line: 52 | 53 | ```yaml 54 | # debugpy: 55 | ``` 56 | 57 | Then launch the task `Run Home Assistant on port 9123`, and launch the debugger 58 | with the existing debugging configuration `Python: Attach Local`. 59 | 60 | For more information, look at [the Remote Python Debugger integration documentation](https://www.home-assistant.io/integrations/debugpy/). 61 | -------------------------------------------------------------------------------- /custom_components/veolia/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Custom integration to integrate Veolia with Home Assistant. 3 | """ 4 | 5 | import asyncio 6 | from datetime import timedelta 7 | import logging 8 | 9 | from homeassistant.config_entries import ConfigEntry 10 | from homeassistant.core import Config, HomeAssistant 11 | from homeassistant.exceptions import ConfigEntryNotReady 12 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 13 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 14 | 15 | from .VeoliaClient import VeoliaClient 16 | from .const import CONF_ABO_ID, CONF_PASSWORD, CONF_USERNAME, DOMAIN, PLATFORMS 17 | from .debug import decoratorexceptionDebug 18 | 19 | SCAN_INTERVAL = timedelta(hours=10) 20 | 21 | _LOGGER = logging.getLogger(__name__) 22 | 23 | 24 | @decoratorexceptionDebug 25 | async def async_setup(hass: HomeAssistant, config: Config): 26 | """Set up this integration using YAML is not supported.""" 27 | return True 28 | 29 | 30 | @decoratorexceptionDebug 31 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 32 | """Set up this integration using UI.""" 33 | if hass.data.get(DOMAIN) is None: 34 | hass.data.setdefault(DOMAIN, {}) 35 | 36 | username = entry.data.get(CONF_USERNAME) 37 | password = entry.data.get(CONF_PASSWORD) 38 | abo_id = entry.data.get(CONF_ABO_ID) 39 | # _LOGGER.debug(f"abo_id={abo_id}") 40 | session = async_get_clientsession(hass) 41 | client = VeoliaClient(username, password, session, abo_id) 42 | coordinator = VeoliaDataUpdateCoordinator(hass, client=client) 43 | await coordinator.async_refresh() 44 | 45 | if not coordinator.last_update_success: 46 | raise ConfigEntryNotReady 47 | 48 | hass.data[DOMAIN][entry.entry_id] = coordinator 49 | 50 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 51 | 52 | return True 53 | 54 | 55 | class VeoliaDataUpdateCoordinator(DataUpdateCoordinator): 56 | """Class to manage fetching data from the API.""" 57 | 58 | def __init__(self, hass: HomeAssistant, client: VeoliaClient) -> None: 59 | """Initialize.""" 60 | self.api = client 61 | self.platforms = [] 62 | 63 | super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) 64 | 65 | async def _async_update_data(self): 66 | """Update data via library.""" 67 | try: 68 | consumption = await self.hass.async_add_executor_job(self.api.update_all) 69 | _LOGGER.debug(f"consumption = {consumption}") 70 | return consumption 71 | 72 | except Exception as exception: 73 | raise UpdateFailed() from exception 74 | 75 | 76 | @decoratorexceptionDebug 77 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): 78 | """Handle removal of an entry.""" 79 | coordinator = hass.data[DOMAIN][entry.entry_id] 80 | unloaded = all( 81 | await asyncio.gather( 82 | *[ 83 | hass.config_entries.async_forward_entry_unload(entry, platform) 84 | for platform in PLATFORMS 85 | if platform in coordinator.platforms 86 | ] 87 | ) 88 | ) 89 | if unloaded: 90 | hass.data[DOMAIN].pop(entry.entry_id) 91 | 92 | return unloaded 93 | 94 | 95 | @decoratorexceptionDebug 96 | async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: 97 | """Reload config entry.""" 98 | await async_unload_entry(hass, entry) 99 | await async_setup_entry(hass, entry) 100 | -------------------------------------------------------------------------------- /custom_components/veolia/sensor.py: -------------------------------------------------------------------------------- 1 | """Sensor platform for Veolia.""" 2 | 3 | import logging 4 | 5 | from homeassistant.components.sensor import SensorStateClass 6 | 7 | from .const import DAILY, DOMAIN, HISTORY, MONTHLY 8 | from .debug import decoratorexceptionDebug 9 | from .entity import VeoliaEntity 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | async def async_setup_entry(hass, entry, async_add_devices): 15 | """Set up sensor platform.""" 16 | coordinator = hass.data[DOMAIN][entry.entry_id] 17 | sensors = [ 18 | VeoliaDailyUsageSensor(coordinator, entry), 19 | VeoliaMonthlyUsageSensor(coordinator, entry), 20 | VeoliaLastIndexSensor(coordinator, entry), 21 | ] 22 | async_add_devices(sensors) 23 | 24 | 25 | class VeoliaLastIndexSensor(VeoliaEntity): 26 | """Monitors the last index.""" 27 | 28 | @property 29 | def name(self): 30 | """Return the name of the sensor.""" 31 | return "veolia_last_index" 32 | 33 | @property 34 | def state_class(self): 35 | """Return the state_class of the sensor.""" 36 | _LOGGER.debug(f"state_class = {SensorStateClass.TOTAL_INCREASING}") 37 | return SensorStateClass.TOTAL_INCREASING 38 | 39 | @property 40 | @decoratorexceptionDebug 41 | def state(self): 42 | """Return the state of the sensor.""" 43 | _LOGGER.debug(f"self.coordinator.data = {self.coordinator.data['last_index']}") 44 | state = self.coordinator.data["last_index"] 45 | if state > 0: 46 | return state 47 | return None 48 | 49 | @property 50 | @decoratorexceptionDebug 51 | def extra_state_attributes(self): 52 | """Return the extra state attributes.""" 53 | return self._base_extra_state_attributes() 54 | 55 | 56 | class VeoliaDailyUsageSensor(VeoliaEntity): 57 | """Monitors the daily water usage.""" 58 | 59 | @property 60 | def name(self): 61 | """Return the name of the sensor.""" 62 | return "veolia_daily_consumption" 63 | 64 | @property 65 | @decoratorexceptionDebug 66 | def state(self): 67 | """Return the state of the sensor.""" 68 | _LOGGER.debug(f"self.coordinator.data = {self.coordinator.data[DAILY]}") 69 | state = self.coordinator.data[DAILY][HISTORY][0][1] 70 | if state > 0: 71 | return state 72 | return None 73 | 74 | @property 75 | @decoratorexceptionDebug 76 | def extra_state_attributes(self): 77 | """Return the extra state attributes.""" 78 | _LOGGER.debug(f"Daily : self.coordinator.data = {self.coordinator.data[DAILY]}") 79 | attrs = self._base_extra_state_attributes() | { 80 | "historyConsumption": self.coordinator.data[DAILY][HISTORY], 81 | } 82 | return attrs 83 | 84 | 85 | class VeoliaMonthlyUsageSensor(VeoliaEntity): 86 | """Monitors the monthly water usage.""" 87 | 88 | @property 89 | def name(self): 90 | """Return the name of the sensor.""" 91 | return "veolia_monthly_consumption" 92 | 93 | @property 94 | def state(self): 95 | """Return the state of the sensor.""" 96 | _LOGGER.debug(f"self.coordinator.data = {self.coordinator.data[MONTHLY]}") 97 | state = self.coordinator.data[MONTHLY][HISTORY][0][1] 98 | if state > 0: 99 | return state 100 | return None 101 | 102 | @property 103 | @decoratorexceptionDebug 104 | def extra_state_attributes(self): 105 | """Return the extra state attributes.""" 106 | _LOGGER.debug(f"Monthly : self.coordinator.data = {self.coordinator.data[MONTHLY]}") 107 | attrs = self._base_extra_state_attributes() | { 108 | "historyConsumption": self.coordinator.data[MONTHLY][HISTORY], 109 | } 110 | return attrs 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Veolia 2 | 3 | [![GitHub Release][releases-shield]][releases] 4 | [![GitHub Activity][commits-shield]][commits] 5 | [![License][license-shield]](LICENSE) 6 | 7 | [![hacs][hacsbadge]][hacs] 8 | [![Project Maintenance][maintenance-shield]][user_profile] 9 | 10 | 11 | > # Ce projet n'est plus maintenu !!!! 12 | > 13 | > # Vous pouvez trouver une alternative ici : 14 | > # [https://github.com/Jezza34000/homeassistant_veolia](https://github.com/Jezza34000/homeassistant_veolia) 15 | 16 | 17 | 18 | 19 | ![logo][logoimg] 20 | 21 | **Ce composant configurera les plates-formes suivantes.** 22 | 23 | Platform | Description 24 | -- | -- 25 | `sensor` | Affichez la consommation quotidienne et mensuelle ainsi que la dernière valeur d'index de l'API Veolia. Ce sensor est de type enregie, il apparaitra donc dans le tableau de bord Energie 26 | 27 | 28 | ![appareil][appareilimg] 29 | 30 | ![daily_consumption][daily_consumptionimg] 31 | 32 | ![monthly_consumption][monthly_consumptionimg] 33 | 34 | 35 | ## Installation 36 | 37 | ### HACS 38 | 39 | Recommandé car vous recevrez des notifications de mises à jour 40 | 41 | [![Ouvrez votre instance Home Assistant et ouvrez ce référentiel dans la boutique communautaire Home Assistant.][my_hacs_badge]][my_ha_link] 42 | 43 | Si le lien ci-dessus ne fonctionne pas, suivez ces étapes : 44 | * Ajoutez ce référentiel https://github.com/kugan49/veolia-custom-component-HACS à HACS en tant que « Dépôts personnalisés » avec la catégorie « Intégration ». Cette option se trouve dans le menu ⋮ 45 | * Installer l'intégration depuis HACS 46 | * Redémarrez Home Assistant 47 | 48 | ### Manuellement 49 | 50 | * Extrayez le fichier Zip dans le répertoire `custom_components` 51 | * Redémarrez Home Assistant 52 | 53 | ## La configuration se fait dans l'interface utilisateur 54 | 55 | Remplissez simplement votre nom d'utilisateur et votre mot de passe lors de l'ajout de l'intégration 56 | 57 | Si vous avez plusieurs compteurs, vous pouvez renseigner la référence abonnement pour récupérer les bon résultats. 58 | Si vous ne la renseignez pas, le premier abonnement sera automatiquement selectionné. 59 | 60 | La référence abonnement se trouve dans l'onglet `Gérer votre Espace Personnel` dans la section `Mes Contrats`, sous la colonne `Références` 61 | 62 | Vous pouvez ajouter autant d'intégration que de compteur à suivre. 63 | 64 | 65 | ## Ajoutez une carte apexcharts pour afficher l'attribut d'historique 66 | 67 | Vous pouvez utiliser [apexcharts-card](https://github.com/RomRider/apexcharts-card) 68 | 69 | ```yaml 70 | 71 | type: custom:apexcharts-card 72 | graph_span: 1month 73 | header: 74 | show: true 75 | title: ApexCharts-Card 76 | show_states: true 77 | colorize_states: true 78 | series: 79 | - entity: sensor.veolia_daily_consumption 80 | type: column 81 | data_generator: | 82 | return entity.attributes.historyConsumption.map((val, index) => { 83 | return [new Date(val[0]).getTime(), val[1]]; 84 | }); 85 | 86 | ``` 87 | 88 | ![apexchartsimg] 89 | 90 | 91 | ## Remerciement spécial 92 | 93 | Un grand merci à [@Pulpyyyy](https://github.com/Pulpyyyy), qui m'a beaucoup aidé dans la recherche du fonctionnement de l'API 94 | 95 | 96 | *** 97 | 98 | [commits-shield]: https://img.shields.io/github/commit-activity/y/kugan49/veolia-custom-component-HACS.svg?style=for-the-badge 99 | [commits]: https://github.com/kugan49/veolia-custom-component-HACS/commits/master 100 | [hacs]: https://hacs.xyz 101 | [hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge 102 | [my_hacs_badge]: https://my.home-assistant.io/badges/hacs_repository.svg 103 | [my_ha_link]: https://my.home-assistant.io/redirect/hacs_repository/?owner=kugan49&repository=veolia-custom-component-HACS&category=integration 104 | [logoimg]: images/logo.png 105 | [appareilimg]: images/appareil.png 106 | [daily_consumptionimg]: images/daily_consumption.png 107 | [monthly_consumptionimg]: images/monthly_consumption.png 108 | [apexchartsimg]: images/apexcharts-card_example.png 109 | [license-shield]: https://img.shields.io/github/license/kugan49/veolia-custom-component-HACS.svg?style=for-the-badge 110 | [maintenance-shield]: https://img.shields.io/badge/maintainer-%40kugan49-blue.svg?style=for-the-badge 111 | [releases-shield]: https://img.shields.io/github/release/kugan49/veolia-custom-component-HACS.svg?style=for-the-badge 112 | [releases]: https://github.com/kugan49/veolia-custom-component-HACS/releases 113 | [user_profile]: https://github.com/kugan49 114 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | 3 | ## How to commit new version : 4 | 5 | - change version in `custom_components/veolia/manifest.json` 6 | - Add infos about new version in `CHANGELOG.md` 7 | - To auto release commit whith name ( :exclamation: case sensitive) : `Release: vx.x.x` 8 | 9 | ## What? 10 | 11 | [integration_blueprint][integration_blueprint] 12 | 13 | This repository contains multiple files, here is a overview: 14 | 15 | | File | Purpose | 16 | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 17 | | `.devcontainer/*` | Used for development/testing with VSCODE, more info in the readme file in that dir. | 18 | | `.github/ISSUE_TEMPLATE/feature_request.md` | Template for Feature Requests | 19 | | `.github/ISSUE_TEMPLATE/issue.md` | Template for issues | 20 | | `.vscode/tasks.json` | Tasks for the devcontainer. | 21 | | `custom_components/veolia/translations/*` | [Translation files.](https://developers.home-assistant.io/docs/internationalization/custom_integration) | 22 | | `custom_components/veolia/__init__.py` | The component file for the integration. | 23 | | `custom_components/veolia/api.py` | This is a sample API client. | 24 | | `custom_components/veolia/binary_sensor.py` | Binary sensor platform for the integration. | 25 | | `custom_components/veolia/config_flow.py` | Config flow file, this adds the UI configuration possibilities. | 26 | | `custom_components/veolia/const.py` | A file to hold shared variables/constants for the entire integration. | 27 | | `custom_components/veolia/manifest.json` | A [manifest file](https://developers.home-assistant.io/docs/en/creating_integration_manifest.html) for Home Assistant. | 28 | | `custom_components/veolia/sensor.py` | Sensor platform for the integration. | 29 | | `custom_components/veolia/switch.py` | Switch sensor platform for the integration. | 30 | | `tests/__init__.py` | Makes the `tests` folder a module. | 31 | | `tests/conftest.py` | Global [fixtures](https://docs.pytest.org/en/stable/fixture.html) used in tests to [patch](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch) functions. | 32 | | `tests/test_api.py` | Tests for `custom_components/veolia/api.py`. | 33 | | `tests/test_config_flow.py` | Tests for `custom_components/veolia/config_flow.py`. | 34 | | `tests/test_init.py` | Tests for `custom_components/veolia/__init__.py`. | 35 | | `tests/test_switch.py` | Tests for `custom_components/veolia/switch.py`. | 36 | | `CONTRIBUTING.md` | Guidelines on how to contribute. | 37 | | `example.png` | Screenshot that demonstrate how it might look in the UI. | 38 | | `info.md` | An example on a info file (used by [hacs][hacs]). | 39 | | `LICENSE` | The license file for the project. | 40 | | `README.md` | The file you are reading now, should contain info about the integration, installation and configuration instructions. | 41 | | `requirements.txt` | Python packages used by this integration. | 42 | | `requirements_dev.txt` | Python packages used to provide [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense)/code hints during development of this integration, typically includes packages in `requirements.txt` but may include additional packages | 43 | | `requirements_test.txt` | Python packages required to run the tests for this integration, typically includes packages in `requirements_dev.txt` but may include additional packages | 44 | 45 | ## How? 46 | 47 | If you want to use all the potential and features of this blueprint template you 48 | should use Visual Studio Code to develop in a container. In this container you 49 | will have all the tools to ease your python development and a dedicated Home 50 | Assistant core instance to run your integration. See `.devcontainer/README.md` for more information. 51 | 52 | If you need to work on the python library in parallel of this integration 53 | (`sampleclient` in this example) there are different options. The following one seems 54 | easy to implement: 55 | 56 | - Create a dedicated branch for your python library on a public git repository (example: branch 57 | `dev` on `https://github.com/ludeeus/sampleclient`) 58 | - Update in the `manifest.json` file the `requirements` key to point on your development branch 59 | ( example: `"requirements": ["git+https://github.com/ludeeus/sampleclient.git@dev#devp==0.0.1beta1"]`) 60 | - Each time you need to make a modification to your python library, push it to your 61 | development branch and increase the number of the python library version in `manifest.json` file 62 | to ensure Home Assistant update the code of the python library. (example `"requirements": ["git+https://...==0.0.1beta2"]`). 63 | 64 | [integration_blueprint]: https://github.com/custom-components/integration_blueprint 65 | [hacs]: https://github.com/custom-components/hacs 66 | -------------------------------------------------------------------------------- /custom_components/veolia/VeoliaClient.py: -------------------------------------------------------------------------------- 1 | """API Program for Veolia.""" 2 | 3 | from copy import deepcopy as copy 4 | from datetime import datetime 5 | import logging 6 | import operator 7 | import xml.etree.ElementTree as ET 8 | 9 | import requests 10 | import xmltodict 11 | 12 | from .const import DAILY, FORMAT_DATE, HISTORY, MONTHLY 13 | 14 | _LOGGER: logging.Logger = logging.getLogger(__package__) 15 | 16 | 17 | class VeoliaError(Exception): 18 | """Error from API.""" 19 | 20 | pass 21 | 22 | 23 | class BadCredentialsException(Exception): 24 | """Wrong authentication.""" 25 | 26 | pass 27 | 28 | 29 | class VeoliaClient: 30 | """Class to manage the webServices system.""" 31 | 32 | def __init__(self, email: str, password: str, session=requests.Session(), abo_id="") -> None: 33 | """Initialize the client object.""" 34 | self._email = email 35 | self._pwd = password 36 | self.__aboId = abo_id 37 | self.address = "https://www.service.eau.veolia.fr/icl-ws/iclWebService" 38 | self.headers = {"Content-Type": "application/xml; charset=UTF-8"} 39 | self.__tokenPassword = None 40 | self.success = False 41 | self.attributes = {DAILY: {}, MONTHLY: {}} 42 | # self.session = session 43 | self.session = requests.Session() 44 | self.__enveloppe = self.__create_enveloppe() 45 | 46 | def login(self): 47 | """Check if login is right. 48 | 49 | raise BadCredentialsException if not 50 | """ 51 | try: 52 | _LOGGER.info("Check credentials") 53 | self._get_tokenPassword(check_only=True) 54 | except Exception as e: 55 | _LOGGER.error(f"wrong authentication : {e}") 56 | raise BadCredentialsException(f"wrong authentication : {e}") 57 | 58 | def update_all(self): 59 | """ 60 | Return the latest collected datas. 61 | 62 | Returns: 63 | dict: dict of consumptions by date and by period 64 | """ 65 | self.update() 66 | self.update(True) 67 | return self.attributes 68 | 69 | def update(self, month=False): 70 | """ 71 | Return the latest collected datas by arg. 72 | 73 | Args: 74 | month (bool, optional): if True returns consumption by Month else by Day. Defaults to False. 75 | 76 | Returns: 77 | dict: dict of consumptions by date 78 | """ 79 | if self.__tokenPassword is None: 80 | self._get_tokenPassword() 81 | self._fetch_data(month) 82 | if not self.success: 83 | return 84 | period = MONTHLY if month is True else DAILY 85 | return self.attributes[period] 86 | 87 | def close_session(self): 88 | """Close current session.""" 89 | self.session.close() 90 | self.session = None 91 | 92 | def _fetch_data(self, month=False): 93 | """Fetch latest data from Veolia.""" 94 | _LOGGER.debug(f"_fetch_data by month ? {month}") 95 | period = MONTHLY if month is True else DAILY 96 | if month is True: 97 | action = "getConsommationMensuelle" 98 | else: 99 | action = "getConsommationJournaliere" 100 | _LOGGER.debug(f"action={action}") 101 | datas = self.__construct_body(action, {"aboNum": self.__aboId}, anonymous=False) 102 | 103 | resp = self.session.post( 104 | self.address, 105 | headers=self.headers, 106 | data=datas, 107 | ) 108 | _LOGGER.debug(str(resp)) 109 | _LOGGER.debug(str(resp.text)) 110 | if resp.status_code != 200: 111 | # Améliorer le retour si erreur 500 : possibilité de récupérer le message du serveur 112 | msg = f"Error {resp.status_code} fetching data :" 113 | try: 114 | msg += xmltodict.parse(f"")[ 115 | "soap:Envelope" 116 | ]["soap:Body"]["soap:Fault"]["faultstring"] 117 | except Exception: 118 | msg += str(resp.text) 119 | _LOGGER.error(msg) 120 | raise Exception(f"{msg}") 121 | else: 122 | try: 123 | result = xmltodict.parse(f"") 124 | _LOGGER.debug(f"result_fetch_data={result}") 125 | lstindex = result["soap:Envelope"]["soap:Body"][f"ns2:{action}Response"]["return"] 126 | self.attributes[period][HISTORY] = [] 127 | 128 | # sort date desc and append in list of tuple (date,liters) 129 | if month is True: 130 | if isinstance(lstindex, list): 131 | lstindex.sort(key=operator.itemgetter("annee", "mois"), reverse=True) 132 | for val in lstindex: 133 | self.attributes[period][HISTORY].append( 134 | ( 135 | f"{val['annee']}-{val['mois']}", 136 | int(val["consommation"]), 137 | ) 138 | ) 139 | elif isinstance(lstindex, dict): 140 | self.attributes[period][HISTORY].append( 141 | ( 142 | f"{lstindex['annee']}-{lstindex['mois']}", 143 | int(lstindex["consommation"]), 144 | ) 145 | ) 146 | 147 | else: 148 | if isinstance(lstindex, list): 149 | lstindex.sort(key=operator.itemgetter("dateReleve"), reverse=True) 150 | for val in lstindex: 151 | self.attributes[period][HISTORY].append( 152 | ( 153 | datetime.strptime(val["dateReleve"], FORMAT_DATE).date(), 154 | int(val["consommation"]), 155 | ) 156 | ) 157 | self.attributes["last_index"] = int(lstindex[0]["index"]) + int(lstindex[0]["consommation"]) 158 | elif isinstance(lstindex, dict): 159 | self.attributes[period][HISTORY].append( 160 | ( 161 | datetime.strptime(lstindex["dateReleve"], FORMAT_DATE).date(), 162 | int(lstindex["consommation"]), 163 | ) 164 | ) 165 | self.attributes["last_index"] = int(lstindex["index"]) + int(lstindex["consommation"]) 166 | self.success = True 167 | except ValueError: 168 | raise VeoliaError("Issue with accessing data") 169 | pass 170 | 171 | def _get_tokenPassword(self, check_only=False): 172 | """Get token password for next actions who needs authentication.""" 173 | datas = self.__construct_body( 174 | "getAuthentificationFront", 175 | {"cptEmail": self._email, "cptPwd": self._pwd}, 176 | anonymous=True, 177 | ) 178 | # _LOGGER.debug(f"_get_token_password : {datas.replace(self._pwd,"MySecretPassWord")}") 179 | resp = self.session.post( 180 | self.address, 181 | headers=self.headers, 182 | data=datas, 183 | ) 184 | _LOGGER.debug(f"resp status={resp.status_code}") 185 | if resp.status_code != 200: 186 | _LOGGER.error("problem with authentication") 187 | raise Exception(f"POST /__get_tokenPassword/ {resp.status_code}") 188 | else: 189 | result = xmltodict.parse(f"") 190 | _LOGGER.debug(f"result_getauth={result}") 191 | if check_only: 192 | return None 193 | self.__tokenPassword = result["soap:Envelope"]["soap:Body"]["ns2:getAuthentificationFrontResponse"][ 194 | "return" 195 | ]["espaceClient"]["cptPwd"] 196 | contrat = result["soap:Envelope"]["soap:Body"]["ns2:getAuthentificationFrontResponse"]["return"][ 197 | "listContrats" 198 | ] 199 | 200 | if self.__aboId == "": 201 | _LOGGER.debug("No Abo_ID provided, finding first") 202 | if isinstance(contrat, list): 203 | self.__aboId = contrat[0]["aboId"] 204 | else: 205 | self.__aboId = contrat["aboId"] 206 | _LOGGER.debug(f"__aboId={self.__aboId}") 207 | # _LOGGER.debug(f"__tokenPassword={self.__tokenPassword}") 208 | 209 | def __create_enveloppe(self): 210 | """Return enveloppe for requests. 211 | 212 | Returns: 213 | xml: enveloppe 214 | """ 215 | # 216 | __enveloppe = ET.Element("soap:Envelope") 217 | __enveloppe.set("xmlns:soap", "http://schemas.xmlsoap.org/soap/envelope/") 218 | __enveloppe.set("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") 219 | # 220 | __header = ET.SubElement(__enveloppe, "soap:Header") 221 | # 222 | __security = ET.SubElement(__header, "wsse:Security") 223 | __security.set( 224 | "xmlns:wsse", 225 | "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", 226 | ) 227 | __security.set( 228 | "xmlns:wsu", 229 | "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", 230 | ) 231 | # 232 | __usernameToken = ET.SubElement(__security, "wsse:UsernameToken") 233 | __usernameToken.set( 234 | "xmlns:wsu", 235 | "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", 236 | ) 237 | __usernameToken.set("wsu:Id", "UsernameToken-aiehdbsf52") 238 | # anonyme 239 | __username = ET.SubElement(__usernameToken, "wsse:Username") 240 | __username.text = "anonyme" 241 | # PYg6fMplCoo19dZVXkn2 242 | __password = ET.SubElement(__usernameToken, "wsse:Password") 243 | __password.set( 244 | "Type", 245 | "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText", 246 | ) 247 | __password.text = "PYg6fMplCoo19dZVXkn2" 248 | # 1dWl+HzD/sJsWzAcDHQX6Q== 249 | __nonce = ET.SubElement(__usernameToken, "wsse:Nonce") 250 | __nonce.set( 251 | "EncodingType", 252 | "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary", 253 | ) 254 | __nonce.text = "1dWl+HzD/sJsWzAcDHQX6Q==" 255 | # 2022-11-22T07:54:00.000Z 256 | __created = ET.SubElement(__usernameToken, "wsse:Created") 257 | __created.text = datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%fZ") 258 | # 259 | ET.SubElement(__enveloppe, "soap:Body") 260 | return __enveloppe 261 | 262 | def __construct_body(self, action: str, elts: dict, anonymous=True): 263 | """Append action into a copy of _enveloppe. 264 | 265 | Args: 266 | action (str): Name of action 267 | elts (dict): elements to insert into action 268 | anonymous (bool, optional): anonymous authentication. Defaults to True. 269 | 270 | Returns: 271 | xml: completed enveloppe for requests 272 | """ 273 | datas = copy(self.__enveloppe) 274 | _body = datas.find("soap:Body") 275 | _action = ET.SubElement(_body, f"ns2:{action}") 276 | _action.set("xmlns:ns2", "http://ws.icl.veolia.com/") 277 | for k, v in elts.items(): 278 | t = ET.SubElement(_action, k) 279 | t.text = v 280 | 281 | if not anonymous: 282 | username_token = datas.find("soap:Header").find("wsse:Security").find("wsse:UsernameToken") 283 | username_token.find("wsse:Username").text = self._email 284 | username_token.find("wsse:Password").text = self.__tokenPassword 285 | 286 | return ET.tostring(datas, encoding="UTF-8") 287 | --------------------------------------------------------------------------------