Skip to content

Commit d289c9b

Browse files
authored
Merge pull request #1 from sebcaps/dev
Dev to main
2 parents 004342f + 8583946 commit d289c9b

18 files changed

Lines changed: 683 additions & 3 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "Tuto dev hacs",
3+
"image": "mcr.microsoft.com/devcontainers/python:0-3.11",
4+
"appPort": [
5+
"9123:8123"
6+
],
7+
"customizations": {
8+
"vscode": {
9+
"extensions": [
10+
"ms-python.python",
11+
"github.vscode-pull-request-github",
12+
"ryanluker.vscode-coverage-gutters",
13+
"ms-python.vscode-pylance"
14+
],
15+
"settings": {
16+
"files.eol": "\n",
17+
"editor.tabSize": 4,
18+
"terminal.integrated.profiles.linux": {
19+
"Bash Profile": {
20+
"path": "bash",
21+
"args": []
22+
}
23+
},
24+
"terminal.integrated.defaultProfile.linux": "Bash Profile",
25+
"python.pythonPath": "/usr/bin/python3",
26+
"python.analysis.autoSearchPaths": true,
27+
"python.linting.pylintEnabled": true,
28+
"python.linting.enabled": true,
29+
"python.formatting.provider": "black",
30+
"editor.formatOnPaste": false,
31+
"editor.formatOnSave": true,
32+
"editor.formatOnType": true,
33+
"files.trimTrailingWhitespace": true
34+
}
35+
}
36+
},
37+
"postCreateCommand": "pip install -r requirements.txt"
38+
}

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ coverage.xml
5050
.hypothesis/
5151
.pytest_cache/
5252
cover/
53-
53+
tests/
5454
# Translations
5555
*.mo
5656
*.pot
@@ -158,3 +158,5 @@ cython_debug/
158158
# and can be added to the global gitignore or merged into this file. For a more nuclear
159159
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
160160
#.idea/
161+
config
162+
requirements.txt

.vscode/launch.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
// Utilisez IntelliSense pour en savoir plus sur les attributs possibles.
3+
// Pointez pour afficher la description des attributs existants.
4+
// Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Home Assistant",
9+
"type": "python",
10+
"request": "launch",
11+
"module": "homeassistant",
12+
"justMyCode": false,
13+
"args": [
14+
"--debug",
15+
"-c",
16+
"config"
17+
]
18+
}
19+
]
20+
}

.vscode/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"editor.formatOnSave": true,
3+
"[python]": {
4+
"editor.defaultFormatter": "ms-python.python"
5+
},
6+
"python.formatting.provider": "none"
7+
}

.vscode/tasks.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"version": "2.0.0",
3+
"tasks": [
4+
{
5+
"label": "Run Home Assistant on port 9123",
6+
"type": "shell",
7+
"command": "ln -s $PWD/custom_components $PWD/config/custom_components ; hass -c ./config",
8+
"problemMatcher": []
9+
},
10+
{
11+
"label": "Restart Home Assistant on port 9123",
12+
"type": "shell",
13+
"command": "pkill hass ; hass -c ./config",
14+
"problemMatcher": []
15+
}
16+
]
17+
}

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,35 @@
1-
# hydrao
2-
Home Assistant integration to get Hydrao data
1+
# Hydrao pour Home Assistant
2+
Intégration pour remonter les informations de ces pommeaux de douches Hydrao dans Home Assistant.
3+
4+
## Installation
5+
6+
Utilisez [hacs](https://hacs.xyz/).
7+
[![Ouvrez votre instance Home Assistant et ouvrez un référentiel dans la boutique communautaire Home Assistant.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=sebcaps&repository=hydrao&category=integration)
8+
9+
## Configuration
10+
11+
### Obtenir une clé d'API
12+
13+
- Faire une demande de clé d'API à Hydrao en utilisant leur [formulaire de support](https://www.hydrao.com/fr/besoin-d-aide-/sav).
14+
15+
### Configuration dans Home Assistant
16+
17+
La méthode de configuration consiste à utiliser l'interface utilisateur.
18+
19+
Il faut tout d'abord saisir ces [identifiants d'accés](#obtenir-une-clé-dapi) à l'API.
20+
21+
![image info](img/authent.png)
22+
23+
En cas de d'installation avec plusieurs pommeaux disponible, une sélection du (des) dispositif(s) est possible.
24+
25+
> **Note**
26+
> Les données sont issues des serveurs Hydrao via leur API. Cette intégration ne se connecte pas aux pommeaux, et ne se substitue pas au téléphone ou la passerelle Hydrao.
27+
28+
### Données
29+
30+
Les données affichées sont :
31+
32+
- Volume moyen en L
33+
- Volume total en L
34+
- Nombre de douche
35+
- Tendance en %

compose-dev.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
services:
2+
app:
3+
entrypoint:
4+
- sleep
5+
- infinity
6+
image: docker/dev-environments-default:stable-1
7+
init: true
8+
volumes:
9+
- type: bind
10+
source: /var/run/docker.sock
11+
target: /var/run/docker.sock
12+
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
""" Les constantes pour l'intégration Tuto HACS """
2+
import logging
3+
from datetime import timedelta, date
4+
from homeassistant.const import Platform
5+
from homeassistant.core import HomeAssistant
6+
from homeassistant.config_entries import ConfigEntry
7+
from homeassistant.helpers.typing import ConfigType
8+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
9+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
10+
from homeassistant.helpers import device_registry as dr, entity_registry as er
11+
from homeassistant.components.sensor import (SensorEntity)
12+
from .api import HydraoAPI
13+
from .const import DOMAIN, PLATFORMS,NAME, REFRESH_INTERVALL, CONF_DEVICES
14+
15+
_LOGGER = logging.getLogger(__name__)
16+
17+
PLATFORM: list[Platform] = [Platform.SENSOR]
18+
19+
20+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
21+
"""Initialisation from a config entry."""
22+
_LOGGER.info(
23+
"Initializing %s integration with plaforms: %s with config: %s",
24+
DOMAIN,
25+
PLATFORMS,
26+
entry,
27+
)
28+
hass.data.setdefault(DOMAIN, {})
29+
api = HydraoAPI(entry.data)
30+
31+
devices: list[str] = entry.options[CONF_DEVICES]
32+
33+
34+
if entry.entry_id not in hass.data[DOMAIN]:
35+
hass.data[DOMAIN][entry.entry_id] = {}
36+
hass.data[DOMAIN][entry.entry_id]["hydraoapi"]= api
37+
for device in devices:
38+
coordinator = HydraoApiCoordinator(hass=hass, config = entry, api=api, uuid=device)
39+
# await coordinator.async_config_entry_first_refresh() # is it mandatory?
40+
hass.data[DOMAIN][entry.entry_id][device] = coordinator
41+
42+
await hass.config_entries.async_forward_entry_setups(entry, [Platform.SENSOR])
43+
44+
_LOGGER.debug("Setup of %s successful", entry.title)
45+
46+
return True
47+
48+
49+
class HydraoApiCoordinator(DataUpdateCoordinator):
50+
"""A coordinator to fetch data from the api only once"""
51+
52+
def __init__(self, hass, config: ConfigType, api, uuid):
53+
super().__init__(
54+
hass,
55+
_LOGGER,
56+
name=NAME, # for logging purpose
57+
update_method=self._update_method,
58+
update_interval=timedelta(minutes=REFRESH_INTERVALL),
59+
)
60+
self.config = config
61+
self.hass = hass
62+
self.api = api
63+
self.uuid = uuid
64+
65+
66+
async def _update_method(self):
67+
data = await self.api.async_get_device_stat(self.uuid)
68+
if data is not None and len(data) > 0:
69+
return True
70+
else:
71+
self.async_set_update_error(
72+
f'No Data from HYDRAO'
73+
)
74+
return False
75+
76+
async def async_unload_entry(self, hass: HomeAssistant, entry: ConfigEntry) -> bool:
77+
"""This method is called to clean all sensors before re-adding them"""
78+
_LOGGER.debug("async_unload_entry method called")
79+
unload_ok = await hass.config_entries.async_unload_platforms(
80+
entry, [Platform.SENSOR]
81+
)
82+
if unload_ok:
83+
hass.data[DOMAIN].pop(entry.entry_id)
84+
return unload_ok

custom_components/hydrao/api.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import logging
2+
import aiohttp
3+
from aiohttp.client import ClientTimeout, ClientError
4+
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_API_KEY
5+
from .const import AUTH_URL, DATA_URL, CLIENT_TIMEOUT,ERROR_MESSAGE_429
6+
7+
8+
_LOGGER = logging.getLogger(__name__)
9+
10+
11+
class HydraoAPI:
12+
"""Api to get Hydrao data"""
13+
14+
def __init__(
15+
self, config, session: aiohttp.ClientSession = None, timeout=CLIENT_TIMEOUT
16+
) -> None:
17+
self._timeout = timeout
18+
if session is not None:
19+
self._session = session
20+
else:
21+
self._session = aiohttp.ClientSession()
22+
self._config = config
23+
self._token = None
24+
self._data = None
25+
self._headers={}
26+
27+
async def async_get_token(self):
28+
"""Get user token to allow request"""
29+
self._headers={
30+
"x-api-key": self._config[CONF_API_KEY],
31+
"Content-type" : "application/json"
32+
}
33+
request = await self._session.post(
34+
AUTH_URL,
35+
headers = self._headers,
36+
json={
37+
"email": self._config[CONF_EMAIL],
38+
"password": self._config[CONF_PASSWORD],
39+
},
40+
)
41+
if request.status == 200:
42+
resp = await request.json()
43+
self._token = resp["access_token"]
44+
_LOGGER.debug("got response %s ", resp)
45+
elif request.status ==429: #Too many request
46+
_LOGGER.error(
47+
ERROR_MESSAGE_429
48+
)
49+
raise ValueError
50+
else:
51+
_LOGGER.error(
52+
ERROR_MESSAGE_429
53+
)
54+
raise ValueError
55+
56+
async def async_get_devices(self):
57+
self._headers["Authorization"]=f"Bearer {self._token}"
58+
request = await self._session.get(
59+
DATA_URL,
60+
headers = self._headers,
61+
)
62+
if request.status == 200:
63+
resp = await request.json()
64+
_LOGGER.debug("got response %s ", resp)
65+
return resp
66+
elif request.status ==429: #Too many request
67+
_LOGGER.error(
68+
ERROR_MESSAGE_429
69+
)
70+
raise ValueError
71+
else:
72+
_LOGGER.error(
73+
"Failed to get devices list info, with status %s ", request.status
74+
)
75+
raise ValueError
76+
77+
async def get_device_details(self, uuid):
78+
if not self._token:
79+
await self.async_get_token()
80+
self._headers["Authorization"]=f"Bearer {self._token}"
81+
request = await self._session.get(
82+
f"{DATA_URL}/{uuid}",
83+
headers = self._headers,
84+
)
85+
if request.status == 200:
86+
resp = await request.json()
87+
_LOGGER.debug("got response %s ", resp)
88+
return resp
89+
elif request.status == 429: #Too many request
90+
_LOGGER.error(
91+
ERROR_MESSAGE_429
92+
)
93+
raise ValueError
94+
else:
95+
_LOGGER.error(
96+
"Failed to get device info, with status %s ", request.status
97+
)
98+
raise ValueError
99+
100+
async def async_get_device_stat(self, uuid):
101+
if not self._token:
102+
await self.async_get_token()
103+
request = await self._session.get(
104+
f"{DATA_URL}/{uuid}/stats",
105+
headers = self._headers,
106+
)
107+
if request.status == 200:
108+
resp = await request.json()
109+
_LOGGER.debug("got response %s ", resp)
110+
self._data= resp
111+
return resp
112+
elif request.status == 429: #Too many request
113+
_LOGGER.error(
114+
ERROR_MESSAGE_429
115+
)
116+
raise ValueError
117+
else:
118+
_LOGGER.error(
119+
"Failed to get devices info, with status %s ", request.status
120+
)
121+
raise ValueError
122+
123+
def get_key(self, key):
124+
"""Get value for the given key in JSON Data"""
125+
if self._data is not None:
126+
return self._data.get(key)
127+
else:
128+
return ""

0 commit comments

Comments
 (0)