Compare commits
10 Commits
39f61f0b34
...
09616dd208
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09616dd208 | ||
|
|
7aa8f97913 | ||
|
|
cf62d1162b | ||
|
|
42648a44aa | ||
|
|
cae4f71b99 | ||
|
|
6c0bea5a80 | ||
|
|
2f1769a3c4 | ||
|
|
8f37c15e07 | ||
|
|
3d97778eb0 | ||
|
|
77727dc166 |
137
README.md
137
README.md
@@ -1 +1,136 @@
|
|||||||
# iDiamant2Mqtt
|
# 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:
|
idiamant:
|
||||||
container_name: idiamant
|
container_name: idiamant
|
||||||
# image: leclubber/idiamant2mqtt
|
image: leclubber/idiamant2mqtt
|
||||||
build: ./idiamant
|
# build: ./idiamant
|
||||||
privileged: true
|
privileged: true
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
- MQTT_PORT=1883
|
- MQTT_PORT=1883
|
||||||
- MQTT_HOST=mqtt
|
- MQTT_HOST=mqtt
|
||||||
- MQTT_TOPIC=homeassistant
|
- MQTT_TOPIC=homeassistant
|
||||||
- IDIAMANT_USER=${USER}
|
|
||||||
- IDIAMANT_PASSWORD=${PASSWORD}
|
|
||||||
- IDIAMANT_CLIENT_ID=${CLIENT_ID}
|
- IDIAMANT_CLIENT_ID=${CLIENT_ID}
|
||||||
- IDIAMANT_CLIENT_SECRET=${CLIENT_SECRET}
|
- 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_PORT 1883
|
||||||
ENV MQTT_HOST mqtt
|
ENV MQTT_HOST mqtt
|
||||||
ENV MQTT_TOPIC homeassistant
|
ENV MQTT_TOPIC homeassistant
|
||||||
ENV IDIAMANT_PULL_STATUS 5
|
|
||||||
WORKDIR /usr/src/idiamant
|
WORKDIR /usr/src/idiamant
|
||||||
|
|
||||||
COPY requirements.txt requirements.txt
|
COPY requirements.txt requirements.txt
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class Constantes():
|
|||||||
mqttTopic = os.getenv('MQTT_TOPIC', "homeassistant")
|
mqttTopic = os.getenv('MQTT_TOPIC', "homeassistant")
|
||||||
mqttUser = os.getenv('MQTT_USER')
|
mqttUser = os.getenv('MQTT_USER')
|
||||||
mqttPassword = os.getenv('MQTT_PASSWORD')
|
mqttPassword = os.getenv('MQTT_PASSWORD')
|
||||||
idiamantUser = os.getenv("IDIAMANT_USER")
|
|
||||||
idiamantPassword = os.getenv("IDIAMANT_PASSWORD")
|
|
||||||
idiamantClientId = os.getenv("IDIAMANT_CLIENT_ID")
|
idiamantClientId = os.getenv("IDIAMANT_CLIENT_ID")
|
||||||
idiamantClientSecret = os.getenv("IDIAMANT_CLIENT_SECRET")
|
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():
|
class iDiamant():
|
||||||
|
|
||||||
access_token = ""
|
access_token = Constantes.idiamantAccessToken
|
||||||
refresh_token = ""
|
refresh_token = Constantes.idiamantRefreshToken
|
||||||
liste_home_id = list()
|
expire_token = 120
|
||||||
|
volets = {}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def getToken():
|
def initDiscovery():
|
||||||
""" Récupération du token sepuis Netatmo """
|
""" Récupération de tous les volets depuis l'API 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']
|
|
||||||
|
|
||||||
url = "https://api.netatmo.com/api/homesdata"
|
url = "https://api.netatmo.com/api/homesdata"
|
||||||
headers = {"Authorization": "Bearer " + iDiamant.access_token}
|
headers = {"Authorization": "Bearer " + iDiamant.access_token}
|
||||||
response = requests.get(url, headers=headers)
|
response = requests.get(url, headers=headers)
|
||||||
jsonStatus = json.loads(response.text)
|
jsonStatus = json.loads(response.text)
|
||||||
homes = jsonStatus['body']['homes']
|
homes = jsonStatus['body']['homes']
|
||||||
for home in 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
|
@staticmethod
|
||||||
def updateToken():
|
def updateToken():
|
||||||
""" Update d'un token en fin de vie """
|
""" 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
|
@staticmethod
|
||||||
def publish(topic, playload, retain=True):
|
def publish(topic, playload, retain=True):
|
||||||
@@ -60,20 +67,13 @@ class iDiamant():
|
|||||||
client.disconnect()
|
client.disconnect()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def initDiscovery():
|
def initMqtt():
|
||||||
""" Publication des config pour discovery """
|
""" Publication des config sur Mqtt """
|
||||||
for home_id in iDiamant.liste_home_id:
|
for volet in iDiamant.volets:
|
||||||
url = "https://api.netatmo.com/api/homestatus?home_id=" + home_id
|
topic = Constantes.mqttTopic + "/cover/" + volet + "/config"
|
||||||
headers = {"Authorization": "Bearer " + iDiamant.access_token}
|
payload = '{'
|
||||||
response = requests.get(url, headers=headers)
|
payload += '"unique_id": "' + volet + '",'
|
||||||
jsonStatus = json.loads(response.text)
|
payload += '"name": "' + iDiamant.volets[volet]['name'] + '",'
|
||||||
modules = jsonStatus['body']['home']['modules']
|
payload += '"command_topic": "' + Constantes.mqttTopic + '/cover/' + volet + '/set"'
|
||||||
for module in modules:
|
payload += '}'
|
||||||
if "NBR" == module['type']:
|
iDiamant.publish(topic, payload)
|
||||||
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)
|
|
||||||
|
|||||||
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 const import Constantes
|
||||||
from idiamant import iDiamant
|
from idiamant import iDiamant
|
||||||
from idiamant2mqtt import iDiamant2Mqtt
|
from mqtt2idiamant import Mqtt2iDiamant
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
iDiamant.getToken()
|
|
||||||
iDiamant.initDiscovery()
|
iDiamant.initDiscovery()
|
||||||
|
iDiamant.initMqtt()
|
||||||
# Temps entre chaque pull >= 2
|
|
||||||
pullTime = Constantes.idiamantPullStatus
|
|
||||||
if pullTime < 2:
|
|
||||||
pullTime = 2
|
|
||||||
|
|
||||||
# Envoie des ordres à iDiamant
|
# Envoie des ordres à iDiamant
|
||||||
# mqtt2idiamant = Mqtt2iDiamant()
|
mqtt2idiamant = Mqtt2iDiamant()
|
||||||
# mqtt2idiamant.start()
|
mqtt2idiamant.start()
|
||||||
|
|
||||||
|
# Refresh du token
|
||||||
|
while True:
|
||||||
|
sleep(iDiamant.expire_token / 2)
|
||||||
|
iDiamant.updateToken()
|
||||||
|
|||||||
Reference in New Issue
Block a user