-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Aitor Iturrioz <[email protected]>
- Loading branch information
0 parents
commit 19ebc79
Showing
12 changed files
with
537 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.git | ||
.vscode | ||
__pycache__ | ||
*.pyc | ||
*.pyo | ||
*.pyd | ||
.Python | ||
venv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.vscode | ||
__pycache__ | ||
venv |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
FROM python:3.7-alpine | ||
|
||
COPY src/requirements.txt / | ||
|
||
RUN apk update && apk upgrade | ||
RUN apk add --no-cache python3-dev gcc libc-dev linux-headers | ||
|
||
RUN pip install -r requirements.txt | ||
|
||
COPY src/ /app | ||
WORKDIR /app | ||
|
||
CMD ["python", "main.py"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2019 Aitor Iturrioz | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# vSmart2Homie gateway | ||
|
||
This service reads boiler and thermostat values from vSmart platform (Vaillant) and exposes the data to an MQTT broker following the Homie V4 convention. | ||
|
||
The data is modeled as: | ||
|
||
- vSmart (Device) | ||
- Thermostat (Node) | ||
- Current Temperature | ||
- Setpoint Temperature | ||
- Setpoint Mode | ||
- System Mode | ||
- Battery | ||
- Outdoor (Node) | ||
- Outdoor Temperature | ||
- Boiler (Node) | ||
- eBus Error | ||
- Boiler Error | ||
- Maintenance Status | ||
- Refill Water | ||
|
||
To connect to the vSmart platform the app needs the following required environment variables ([instructions](https://github.com/pjmaenh/home-assistant-vaillant#installation-and-configuration)): | ||
|
||
- CLIENT_ID | ||
- CLIENT_SECRET | ||
- USERNAME | ||
- PASSWORD | ||
|
||
Aditionally, the (optional) MQTT connection parameters can be stablished using: | ||
|
||
- MQTT_BROKER (default: localhost) | ||
- MQTT_PORT (default: 1883) | ||
- MQTT_USER (default: None) | ||
- MQTT_PASSWORD (default: None) | ||
- MQTT_CLIENT_ID (default: vsmart2homie) | ||
|
||
Finally, the project is based on the following libraries: | ||
|
||
- [Homie4](https://github.com/mjcumming/homie4) Thanks @mjcumming! | ||
- [pyvaillant](https://github.com/pjmaenh/pyvaillant) Thanks @pjmaenh! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import logging | ||
import os | ||
import time | ||
import sys | ||
|
||
from pyvaillant.client_auth import ClientAuth | ||
from pyvaillant.vaillant_data import VaillantData | ||
from vsmart_device import vSmartDevice | ||
|
||
logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) | ||
|
||
logger = logging.getLogger() | ||
|
||
mqtt_settings = { | ||
"MQTT_BROKER" : "localhost", | ||
"MQTT_PORT" : 1883, | ||
"MQTT_USERNAME": None, | ||
"MQTT_PASSWORD": None, | ||
"MQTT_CLIENT_ID": "vsmart2homie" | ||
} | ||
|
||
if __name__ == "__main__": | ||
try: | ||
# Get vSmart connection values | ||
client_id = os.environ.get("CLIENT_ID") | ||
client_secret = os.environ.get("CLIENT_SECRET") | ||
username = os.environ.get("USERNAME") | ||
password = os.environ.get("PASSWORD") | ||
|
||
if not client_id or not client_secret or not username or not password: | ||
logger.error("Not all vSmart parameters are set") | ||
sys.exit(0) | ||
|
||
# Get mqtt connection values | ||
for value in ["MQTT_BROKER", "MQTT_PORT", "MQTT_USERNAME", "MQTT_PASSWORD", "MQTT_CLIENT_ID"]: | ||
mqtt_settings[value] = os.environ.get(value) or mqtt_settings[value] | ||
|
||
clientAuth = ClientAuth(client_id, client_secret, username, password) | ||
vaillant_connection = VaillantData(clientAuth) | ||
|
||
# Get data interval value | ||
data_interval = int(os.environ.get("DATA_INTERVAL")) or 300 | ||
|
||
system_name = "vSmart ({})".format(vaillant_connection.station_name) | ||
vsmart = vSmartDevice(device_id="vsmart", name=system_name, mqtt_settings=mqtt_settings, connection=vaillant_connection) | ||
|
||
while True: | ||
vaillant_connection.update() | ||
vsmart.get_node("thermostat").set_property_value("current-temp", vaillant_connection.current_temp) | ||
vsmart.get_node("thermostat").set_property_value("setpoint-temp", vaillant_connection.setpoint_temp) | ||
vsmart.get_node("thermostat").set_property_value("setpoint-mode", vaillant_connection.setpoint_mode) | ||
vsmart.get_node("thermostat").set_property_value("system-mode", vaillant_connection.system_mode) | ||
vsmart.get_node("thermostat").set_property_value("battery", vaillant_connection.battery) | ||
|
||
vsmart.get_node("outdoor").set_property_value("outdoor-temp", vaillant_connection.outdoor_temperature) | ||
|
||
vsmart.get_node("boiler").set_property_value("ebus-error", vaillant_connection.ebus_error) | ||
vsmart.get_node("boiler").set_property_value("boiler-error", vaillant_connection.boiler_error) | ||
vsmart.get_node("boiler").set_property_value("maintenance-status", vaillant_connection.maintenance_status) | ||
vsmart.get_node("boiler").set_property_value("refill-water", vaillant_connection.refill_water) | ||
|
||
time.sleep(data_interval) | ||
|
||
except (KeyboardInterrupt, SystemExit): | ||
logger.info("Exiting.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2018 Hugo DUPRAS | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import logging | ||
import requests | ||
import time | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
# Common definitions | ||
_BASE_URL = "https://api.netatmo.com/" | ||
|
||
|
||
class NoDevice(Exception): | ||
pass | ||
|
||
class NoValidMode(Exception): | ||
pass | ||
|
||
class NoValidSystemMode(Exception): | ||
pass | ||
|
||
# Utilities routines | ||
|
||
|
||
def postRequest(url, params=None, timeout=10): | ||
resp = requests.post(url, data=params, timeout=timeout) | ||
if not resp.ok: | ||
logger.error("The Netatmo API returned %s", resp.status_code) | ||
try: | ||
return ( | ||
resp.json() | ||
if "application/json" in resp.headers.get("content-type") | ||
else resp.content | ||
) | ||
except TypeError: | ||
logger.debug("Invalid response %s", resp) | ||
return None | ||
|
||
|
||
def toTimeString(value): | ||
return time.strftime("%Y-%m-%d_%H:%M:%S", time.localtime(int(value))) | ||
|
||
|
||
def toEpoch(value): | ||
return int(time.mktime(time.strptime(value, "%Y-%m-%d_%H:%M:%S"))) | ||
|
||
|
||
def todayStamps(): | ||
today = time.strftime("%Y-%m-%d") | ||
today = int(time.mktime(time.strptime(today, "%Y-%m-%d"))) | ||
return today, today + 3600 * 24 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# Python library for managing a Vaillant / Bulex MiGo Thermostat | ||
# Based on the pyatmo library by Philippe Larduinat | ||
# Revised July 2019 | ||
# Public domain source code | ||
""" | ||
This API provides access to the Vaillant / Bulex smart thermostat | ||
This package can be used with Python3 applications | ||
PythonAPI Vaillant/Bulex REST data access | ||
""" | ||
import logging | ||
import time | ||
|
||
from . import _BASE_URL, NoDevice, postRequest | ||
|
||
LOG = logging.getLogger(__name__) | ||
|
||
# Common definitions | ||
_AUTH_REQ = _BASE_URL + "oauth2/token" | ||
_WEBHOOK_URL_ADD = _BASE_URL + "api/addwebhook" | ||
_WEBHOOK_URL_DROP = _BASE_URL + "api/dropwebhook" | ||
_DEFAULT_SCOPE = "read_station read_camera access_camera read_thermostat write_thermostat read_presence access_presence" | ||
_DEFAULT_APP_VERSION = "1.0.4.0" | ||
_DEFAULT_USER_PREFIX = "vaillant" | ||
|
||
class ClientAuth(object): | ||
""" | ||
Request authentication and keep access token available through token method. Renew it automatically if necessary | ||
Args: | ||
clientId (str): Application clientId delivered by Netatmo on dev.netatmo.com | ||
clientSecret (str): Application Secret key delivered by Netatmo on dev.netatmo.com | ||
username (str) | ||
password (str) | ||
scope (Optional[str]): Default value is 'read_station' | ||
read_station: to retrieve weather station data (Getstationsdata, Getmeasure) | ||
read_camera: to retrieve Welcome data (Gethomedata, Getcamerapicture) | ||
access_camera: to access the camera, the videos and the live stream. | ||
read_thermostat: to retrieve thermostat data (Getmeasure, Getthermostatsdata) | ||
write_thermostat: to set up the thermostat (Syncschedule, Setthermpoint) | ||
read_presence: to retrieve Presence data (Gethomedata, Getcamerapicture) | ||
read_homecoach: to retrieve Home Coache data (Gethomecoachsdata) | ||
access_presence: to access the live stream, any video stored on the SD card and to retrieve Presence's lightflood status | ||
Several value can be used at the same time, ie: 'read_station read_camera' | ||
""" | ||
|
||
def __init__( | ||
self, clientId, clientSecret, username, password, scope=_DEFAULT_SCOPE, app_version=_DEFAULT_APP_VERSION, user_prefix=_DEFAULT_USER_PREFIX | ||
): | ||
postParams = { | ||
"grant_type": "password", | ||
"client_id": clientId, | ||
"client_secret": clientSecret, | ||
"username": username, | ||
"password": password, | ||
"scope": scope, | ||
} | ||
|
||
if user_prefix: | ||
postParams.update({"user_prefix": user_prefix}) | ||
if app_version: | ||
postParams.update({"app_version": app_version}) | ||
|
||
resp = postRequest(_AUTH_REQ, postParams) | ||
self._clientId = clientId | ||
self._clientSecret = clientSecret | ||
try: | ||
self._accessToken = resp["access_token"] | ||
except (KeyError): | ||
LOG.error("Netatmo API returned %s", resp["error"]) | ||
raise NoDevice("Authentication against Netatmo API failed") | ||
self.refreshToken = resp["refresh_token"] | ||
self._scope = resp["scope"] | ||
self.expiration = int(resp["expire_in"] + time.time() - 1800) | ||
|
||
|
||
@property | ||
def accessToken(self): | ||
|
||
if self.expiration < time.time(): # Token should be renewed | ||
postParams = { | ||
"grant_type": "refresh_token", | ||
"refresh_token": self.refreshToken, | ||
"client_id": self._clientId, | ||
"client_secret": self._clientSecret, | ||
} | ||
resp = postRequest(_AUTH_REQ, postParams) | ||
self._accessToken = resp["access_token"] | ||
self.refreshToken = resp["refresh_token"] | ||
self.expiration = int(resp["expire_in"] + time.time() - 1800) | ||
return self._accessToken |
Oops, something went wrong.