Compare commits
10 Commits
39f61f0b34
...
09616dd208
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09616dd208 | ||
|
|
7aa8f97913 | ||
|
|
cf62d1162b | ||
|
|
42648a44aa | ||
|
|
cae4f71b99 | ||
|
|
6c0bea5a80 | ||
|
|
2f1769a3c4 | ||
|
|
8f37c15e07 | ||
|
|
3d97778eb0 | ||
|
|
77727dc166 |
135
README.md
135
README.md
@@ -1 +1,136 @@
|
||||
# iDiamant2Mqtt
|
||||
|
||||
Outil de conversion de l'API du iDiamant (volets Bubendorff) depuis MQTT pour Home Assistant.
|
||||
|
||||
## Netatmo
|
||||
Pour que cela fonctionne, les volets doivent être configurés dans l'application "Home + Control" de Legrand - Netatmo - Bticino.
|
||||
Rendez-vous ensuite sur la page https://dev.netatmo.com/ et connectez-vous avec les même identifiants que l'application. Il faut enuite cliquer sur son login puis "My apps".
|
||||
Cliquer ensuite sur "Create" et renseiger les champs suivants :
|
||||
- app name : le nom de votre application, par exemple "Volets"
|
||||
- description : une courte descrition, pzr exemple "Gestion des volets depuis HASS"
|
||||
- data protection officer name : votre nom
|
||||
- data protection officer email : l'email de votre compte
|
||||
|
||||
Cliquer sur "I agree to the terms and conditions" et sur Save.
|
||||
Dans la partie "App Technical Parameters", conserver les identifiants "client ID" et "client secret" pour le déploiement.
|
||||
|
||||
Il faut ensuite générer un token en allant dans la dernière partie "Token generator" :
|
||||
- Dans "Choose scopes" sélectionner :
|
||||
- read_bubendorff
|
||||
- write_bubendorff
|
||||
- Cliquer sur "Generate Token"
|
||||
- Noter les champs "Access Token" et "Refresh Token" pour le déploiement
|
||||
|
||||
|
||||
## Déploiement
|
||||
|
||||
Il vous faut :
|
||||
|
||||
- Home-Assistant (en même temps, vous êtes surtout là pour lui non?)
|
||||
- Un brocker (serveur) MQTT (sans certificat pour l'instant)
|
||||
- Le service iDiamant2Mqtt
|
||||
|
||||
Il y a deux solutions pour déployer ce service :
|
||||
|
||||
- Docker (recommandé)
|
||||
- Exécuter le script python directement
|
||||
|
||||
### Docker
|
||||
|
||||
Je préfère cette solution car elle encapsule le processus, contient toutes les dépendances et facilite le déploiement.
|
||||
|
||||
Le service peut être démarré grâce à la commande suivante :
|
||||
|
||||
``` sh
|
||||
docker run -d --name idiamant \
|
||||
-e IDIAMANT_CLIENT_ID=abcde \
|
||||
-e IDIAMANT_CLIENT_SECRET=abcdefgh \
|
||||
-e IDIAMANT_ACCESS_TOKEN=accesstokennetatmo \
|
||||
-e IDIAMANT_REFRESH_TOKEN=refreshtokennetatmo \
|
||||
leclubber/idiamant2mqtt
|
||||
```
|
||||
|
||||
Ou en docker-compose (recommandé) :
|
||||
|
||||
``` yaml
|
||||
version: '3'
|
||||
services:
|
||||
idiamant:
|
||||
container_name: idiamant
|
||||
image: leclubber/idiamant2mqtt
|
||||
privileged: true
|
||||
restart: always
|
||||
environment:
|
||||
- MQTT_PORT=1883
|
||||
- MQTT_HOST=mqtt
|
||||
- MQTT_TOPIC=homeassistant
|
||||
- IDIAMANT_CLIENT_ID=<client ID généré>
|
||||
- IDIAMANT_CLIENT_SECRET=<client secret généré>
|
||||
- IDIAMANT_ACCESS_TOKEN=<access token généré>
|
||||
- IDIAMANT_REFRESH_TOKEN=<refresh token généré>
|
||||
```
|
||||
|
||||
Certaines variables d'environnement sont optionnelles, elles possèdent une valeur par défaut. Toutes les variables d'environnement sont les suivantes :
|
||||
|
||||
- MQTT_PORT (1883 par défaut)
|
||||
- MQTT_HOST (mqtt par défaut)
|
||||
- MQTT_TOPIC (homeassistant par défaut)
|
||||
- MQTT_USER (vide par défaut)
|
||||
- MQTT_PASSWORD (vide par défaut)
|
||||
- IDIAMANT_CLIENT_ID (aucun par défaut)
|
||||
- IDIAMANT_CLIENT_SECRET (aucun par défaut)
|
||||
- IDIAMANT_ACCESS_TOKEN (aucun par défaut)
|
||||
- IDIAMANT_REFRESH_TOKEN (aucun par défaut)
|
||||
|
||||
Un fichier [docker-compose.yml](docker-compose.yml) est disponible pour exemple, avec toutes les variables d'environnement ainsi que les services homeassistant et mqtt.
|
||||
|
||||
Une fois votre fichier docker-compose.yml réalisé, il faut lancer la commande suivante pour démarrer le ou les services configurés :
|
||||
|
||||
``` sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
Il faut récupérer le contenu du dossier [idiamant](idiamant) et le mettre sur votre futur serveur.
|
||||
|
||||
Certaines variables d'environnement sont optionnelles, elles possèdent une valeur par défaut (voir section Docker).
|
||||
Chaque variable sera définie de cette manière :
|
||||
|
||||
``` sh
|
||||
export ENV_VAR=valeur
|
||||
```
|
||||
|
||||
Exécuter ensuite les lignes suivantes :
|
||||
|
||||
``` sh
|
||||
pip install -r requirements.txt
|
||||
chmod +x *.py
|
||||
./server.py
|
||||
```
|
||||
|
||||
## Configuration de Home-Assistant
|
||||
|
||||
Le iDiamant dans Home-Assistant fonctionne avec le discovery de MQTT. Il suffit de s'abonner dans Home-Assistant au canal souhaité ("homeassistant" par défaut).
|
||||
|
||||
**Optionnel :** On peut également le déclarer via le fichier de configuration [configuration.yaml](configuration.yaml). Il faut ajouter un module "cover" de cette façon :
|
||||
``` yaml
|
||||
cover:
|
||||
TODO
|
||||
```
|
||||
|
||||
Vous pourrez alors :
|
||||
- Monter, descendre et arrêter les volets
|
||||
- Renommer dans Home-Assistant chaque volet
|
||||
|
||||
## Todo list
|
||||
|
||||
- [x] Initialisation des config dans MQTT
|
||||
- [x] Discovery MQTT
|
||||
- [x] Documentation
|
||||
- [x] Utilisation d'un login/mot de passe pour le broker MQTT
|
||||
- [x] Publier image Docker en multiple arch
|
||||
- [x] Renouvellement du token
|
||||
- [ ] Attente du serveur MQTT si non disponible
|
||||
- [ ] Tester les paramètres et gestion d'erreur
|
||||
- [ ] Utilisation de certificat pour le broker MQTT
|
||||
|
||||
@@ -25,17 +25,16 @@ services:
|
||||
|
||||
idiamant:
|
||||
container_name: idiamant
|
||||
# image: leclubber/idiamant2mqtt
|
||||
build: ./idiamant
|
||||
image: leclubber/idiamant2mqtt
|
||||
# build: ./idiamant
|
||||
privileged: true
|
||||
restart: always
|
||||
environment:
|
||||
- MQTT_PORT=1883
|
||||
- MQTT_HOST=mqtt
|
||||
- MQTT_TOPIC=homeassistant
|
||||
- IDIAMANT_USER=${USER}
|
||||
- IDIAMANT_PASSWORD=${PASSWORD}
|
||||
- IDIAMANT_CLIENT_ID=${CLIENT_ID}
|
||||
- IDIAMANT_CLIENT_SECRET=${CLIENT_SECRET}
|
||||
- IDIAMANT_PULL_STATUS=5
|
||||
- IDIAMANT_ACCESS_TOKEN=${ACCESS_TOKEN}
|
||||
- IDIAMANT_REFRESH_TOKEN=${REFRESH_TOKEN}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ FROM python:3-alpine
|
||||
ENV MQTT_PORT 1883
|
||||
ENV MQTT_HOST mqtt
|
||||
ENV MQTT_TOPIC homeassistant
|
||||
ENV IDIAMANT_PULL_STATUS 5
|
||||
WORKDIR /usr/src/idiamant
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
|
||||
@@ -12,8 +12,8 @@ class Constantes():
|
||||
mqttTopic = os.getenv('MQTT_TOPIC', "homeassistant")
|
||||
mqttUser = os.getenv('MQTT_USER')
|
||||
mqttPassword = os.getenv('MQTT_PASSWORD')
|
||||
idiamantUser = os.getenv("IDIAMANT_USER")
|
||||
idiamantPassword = os.getenv("IDIAMANT_PASSWORD")
|
||||
idiamantClientId = os.getenv("IDIAMANT_CLIENT_ID")
|
||||
idiamantClientSecret = os.getenv("IDIAMANT_CLIENT_SECRET")
|
||||
idiamantPullStatus = int(os.getenv("IDIAMANT_PULL_STATUS", 5))
|
||||
idiamantAccessToken = os.getenv("IDIAMANT_ACCESS_TOKEN")
|
||||
idiamantRefreshToken = os.getenv("IDIAMANT_REFRESH_TOKEN")
|
||||
idiamantExpireToken = 60
|
||||
|
||||
@@ -10,44 +10,51 @@ from time import sleep
|
||||
|
||||
class iDiamant():
|
||||
|
||||
access_token = ""
|
||||
refresh_token = ""
|
||||
liste_home_id = list()
|
||||
access_token = Constantes.idiamantAccessToken
|
||||
refresh_token = Constantes.idiamantRefreshToken
|
||||
expire_token = 120
|
||||
volets = {}
|
||||
|
||||
@staticmethod
|
||||
def getToken():
|
||||
""" Récupération du token sepuis Netatmo """
|
||||
url = "https://api.netatmo.com/oauth2/token"
|
||||
data = {'grant_type': 'password',
|
||||
'username': Constantes.idiamantUser,
|
||||
'password': Constantes.idiamantPassword,
|
||||
'client_id': Constantes.idiamantClientId,
|
||||
'client_secret': Constantes.idiamantClientSecret,
|
||||
'scope': 'read_bubendorff write_bubendorff'
|
||||
}
|
||||
response = requests.post(url, data)
|
||||
while 200 != response.status_code:
|
||||
attente = 20
|
||||
print("Problème d'accès au token : attente de " + attente + " secondes")
|
||||
sleep(attente)
|
||||
response = requests.post(url, data)
|
||||
|
||||
jsonStatus = json.loads(response.text)
|
||||
iDiamant.access_token = jsonStatus['access_token']
|
||||
iDiamant.refresh_token = jsonStatus['refresh_token']
|
||||
|
||||
def initDiscovery():
|
||||
""" Récupération de tous les volets depuis l'API Netatmo """
|
||||
url = "https://api.netatmo.com/api/homesdata"
|
||||
headers = {"Authorization": "Bearer " + iDiamant.access_token}
|
||||
response = requests.get(url, headers=headers)
|
||||
jsonStatus = json.loads(response.text)
|
||||
homes = jsonStatus['body']['homes']
|
||||
for home in homes:
|
||||
iDiamant.liste_home_id.append(home['id'])
|
||||
|
||||
if 'modules' in home:
|
||||
home_id = home['id']
|
||||
modules = home['modules']
|
||||
for module in modules:
|
||||
if "NBR" == module['type']:
|
||||
iDiamant.volets[module['id']] = {
|
||||
'name':module['name'],
|
||||
'bridge':module['bridge'],
|
||||
'id_home':home_id
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def updateToken():
|
||||
""" Update d'un token en fin de vie """
|
||||
url = "https://api.netatmo.com/oauth2/token"
|
||||
data = {'grant_type': 'refresh_token',
|
||||
'refresh_token': iDiamant.refresh_token,
|
||||
'client_id': Constantes.idiamantClientId,
|
||||
'client_secret': Constantes.idiamantClientSecret
|
||||
}
|
||||
response = requests.post(url, data)
|
||||
while 200 != response.status_code:
|
||||
attente = 20
|
||||
print("Problème d'accès au renouvellement de token : attente de " + attente + " secondes")
|
||||
sleep(attente)
|
||||
response = requests.post(url, data)
|
||||
|
||||
jsonStatus = json.loads(response.text)
|
||||
iDiamant.access_token = jsonStatus['access_token']
|
||||
iDiamant.refresh_token = jsonStatus['refresh_token']
|
||||
iDiamant.expire_token = int(jsonStatus['expires_in'])
|
||||
|
||||
@staticmethod
|
||||
def publish(topic, playload, retain=True):
|
||||
@@ -60,20 +67,13 @@ class iDiamant():
|
||||
client.disconnect()
|
||||
|
||||
@staticmethod
|
||||
def initDiscovery():
|
||||
""" Publication des config pour discovery """
|
||||
for home_id in iDiamant.liste_home_id:
|
||||
url = "https://api.netatmo.com/api/homestatus?home_id=" + home_id
|
||||
headers = {"Authorization": "Bearer " + iDiamant.access_token}
|
||||
response = requests.get(url, headers=headers)
|
||||
jsonStatus = json.loads(response.text)
|
||||
modules = jsonStatus['body']['home']['modules']
|
||||
for module in modules:
|
||||
if "NBR" == module['type']:
|
||||
id_volet = module['id']
|
||||
topic = Constantes.mqttTopic + "/cover/" + id_volet + "/config"
|
||||
payload = '{'
|
||||
payload += '"command_topic": "' + Constantes.mqttTopic + '/cover/' + id_volet + '/set",'
|
||||
payload += '"unique_id": "' + id_volet + '"'
|
||||
payload += '}'
|
||||
iDiamant.publish(topic, payload)
|
||||
def initMqtt():
|
||||
""" Publication des config sur Mqtt """
|
||||
for volet in iDiamant.volets:
|
||||
topic = Constantes.mqttTopic + "/cover/" + volet + "/config"
|
||||
payload = '{'
|
||||
payload += '"unique_id": "' + volet + '",'
|
||||
payload += '"name": "' + iDiamant.volets[volet]['name'] + '",'
|
||||
payload += '"command_topic": "' + Constantes.mqttTopic + '/cover/' + volet + '/set"'
|
||||
payload += '}'
|
||||
iDiamant.publish(topic, payload)
|
||||
|
||||
70
idiamant/mqtt2idiamant.py
Normal file
70
idiamant/mqtt2idiamant.py
Normal file
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
# @Author: Jérémy BRAUD
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import requests
|
||||
from const import Constantes
|
||||
from threading import Thread
|
||||
|
||||
from idiamant import iDiamant
|
||||
|
||||
class Mqtt2iDiamant(Thread):
|
||||
""" Thread chargé de la connexion au broker MQTT """
|
||||
|
||||
def __init__(self):
|
||||
Thread.__init__(self)
|
||||
|
||||
def on_connect(self, client, userdata, flags, rc):
|
||||
""" Abonnement aux topics """
|
||||
affichage = "Connected to MQTT with result code " + str(rc)
|
||||
print(affichage)
|
||||
topic = Constantes.mqttTopic + '/cover/+/set'
|
||||
client.subscribe(topic)
|
||||
|
||||
def on_message(self, client, userdata, msg):
|
||||
""" Traitement du message recu """
|
||||
url = "https://api.netatmo.com/api/setstate"
|
||||
headers = {"Authorization": "Bearer " + iDiamant.access_token}
|
||||
topic = str(msg.topic)
|
||||
id_volet = topic.replace(Constantes.mqttTopic + '/cover/', '').replace('/set', '')
|
||||
|
||||
payload = str(msg.payload, encoding="utf-8")
|
||||
position = -1
|
||||
match payload:
|
||||
case 'OPEN':
|
||||
position = 100
|
||||
case 'CLOSE':
|
||||
position = 0
|
||||
case _:
|
||||
position = -1
|
||||
|
||||
data = {
|
||||
"home": {
|
||||
"id": iDiamant.volets[id_volet]['id_home'],
|
||||
"modules": [
|
||||
{
|
||||
"id": id_volet,
|
||||
"target_position": position,
|
||||
"bridge": iDiamant.volets[id_volet]['bridge']
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Appel de l'API
|
||||
response = requests.post(url, json=data, headers=headers)
|
||||
if 200 != response.status_code:
|
||||
print("Erreur : " + response.content)
|
||||
|
||||
def run(self):
|
||||
""" Démarrage du service MQTT """
|
||||
client = mqtt.Client()
|
||||
if Constantes.mqttUser:
|
||||
client.username_pw_set(Constantes.mqttUser, Constantes.mqttPassword)
|
||||
client.on_connect = self.on_connect
|
||||
client.on_message = self.on_message
|
||||
client.connect(Constantes.mqttHost, Constantes.mqttPort, 60)
|
||||
client.loop_forever()
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@
|
||||
|
||||
from const import Constantes
|
||||
from idiamant import iDiamant
|
||||
from idiamant2mqtt import iDiamant2Mqtt
|
||||
from mqtt2idiamant import Mqtt2iDiamant
|
||||
from time import sleep
|
||||
|
||||
iDiamant.getToken()
|
||||
iDiamant.initDiscovery()
|
||||
|
||||
# Temps entre chaque pull >= 2
|
||||
pullTime = Constantes.idiamantPullStatus
|
||||
if pullTime < 2:
|
||||
pullTime = 2
|
||||
iDiamant.initMqtt()
|
||||
|
||||
# Envoie des ordres à iDiamant
|
||||
# mqtt2idiamant = Mqtt2iDiamant()
|
||||
# mqtt2idiamant.start()
|
||||
mqtt2idiamant = Mqtt2iDiamant()
|
||||
mqtt2idiamant.start()
|
||||
|
||||
# Refresh du token
|
||||
while True:
|
||||
sleep(iDiamant.expire_token / 2)
|
||||
iDiamant.updateToken()
|
||||
|
||||
Reference in New Issue
Block a user