Hello,
I’d like to develop myVaillant connector. There is already a python library wrapping perfectly the myVaillant services. I have some problems just on the login, I put a test code here (it does do exactly the same of myPyllant project )
I get “Your request was automatically detected as a potential threat.” In Python I don’t have any problem, can anybody help me to know what I’m missing so I can try to develop the binding?
Thanks in advance
You might need to ask on the myPylian project (look through the issues, they may have see the same). That error looks like it’s coming from the Vailant API and it’s rejecting the connection because it’s suspicious.
I doubt if cookie is an issue, it is browser thingy used only for SSO cases (after first login). What you do mimic is ouath flow (apparently against Keycloak which is OpenID Connect compatible solution).
There is number of things which might go wrong with oauth setup. Are you sure that all parameters (client-id, redirect uri rec) do match?
What message do you get from Vaillant authentication service?
Your request was automatically detected as a potential threat.
with the same python request using aiohttp the request is going good:
async def login(self):
"""
This should really be done in the browser with OIDC, but that's not easy without support from Vaillant
So instead, we grab the login endpoint from the HTML form of the login website and send username + password
to obtain a session
"""
code_verifier, code_challenge = generate_code()
auth_querystring = {
"response_type": "code",
"client_id": CLIENT_ID,
"code": "code_challenge",
"redirect_uri": "enduservaillant.page.link://login",
"code_challenge_method": "S256",
"code_challenge": code_challenge,
}
# Grabbing the login URL from the HTML form of the login page
try:
async with self.aiohttp_session.get(
AUTHENTICATE_URL.format(realm=get_realm(self.brand, self.country))
+ "?"
+ urlencode(auth_querystring)
) as resp:
login_html = await resp.text()
except ClientResponseError as e:
raise LoginEndpointInvalid from e
result = re.search(
LOGIN_URL.format(realm=get_realm(self.brand, self.country)) + r"\?([^\"]*)",
login_html,
)
login_url = unescape(result.group())
logger.debug(f"Got login url {login_url}")
login_payload = {
"username": self.username,
"password": self.password,
"credentialId": "",
}
# Obtaining the code
async with self.aiohttp_session.post(
login_url, data=login_payload, allow_redirects=False
) as resp:
logger.debug(f"Got login response headers {resp.headers}")
if "Location" not in resp.headers:
raise AuthenticationFailed("Login failed")
logger.debug(
f'Got location from authorize endpoint: {resp.headers["Location"]}'
)
parsed_url = urlparse(resp.headers["Location"])
code = parse_qs(parsed_url.query)["code"]
Second request to authorization server in code flow is exchange of auth code to a token thus you may need more parameters than just user/password. From what I remember this step in oauth is secured with client credentials (which are optional for public clients such as mobile app).
Login flow is different with Keycloack on Vaillant in this case. The Python client sends the request only passing that params to the form he got from the first request (I get the same html too) but the link given accept username and password (It’s a standard form) on python all is working good only with that params:
login_payload = {
"username": self.username,
"password": self.password,
"credentialId": "",
}
# Obtaining the code
async with self.aiohttp_session.post(
login_url, data=login_payload, allow_redirects=False
) as resp:
logger.debug(f"Got login response headers {resp.headers}")
if "Location" not in resp.headers:
raise AuthenticationFailed("Login failed")
logger.debug(
f'Got location from authorize endpoint: {resp.headers["Location"]}'
)
parsed_url = urlparse(resp.headers["Location"])
code = parse_qs(parsed_url.query)["code"]
I think there is something wrong with the cookie managment, may be I have to try with the OkHttp3 client
You’re right, looking closer at the code I see that auth code is exchanged in third request, not in second. This means that HTML form you receive is a customization of Keycloak login flow which requires proper maintenance of already started Keycloak session. In order to do so you need to look carefully at cookies named AUTH_SESSION_ID or similar. Somewhere there should be also a “tab_id” parameter which is essential on Keycloak end to properly reclaim session. I have not looked into Keycloak cookie management since a while, but from what I remember it was able to ship new cookies between first screen (login form) and second one (consent screen). Have a look on behavior of http client.
Although error message you get does not come from Keycloak it might be that some part of low level Keycloak infrastructure is unhappy with your request.
I got the phyton script runnig without any problems too. @giorginus80: Did you made any progress with your java code? May be the openhab “connectorio” brings some ideas, how to handle the oauth?
Hello sorry for the late response, I missed notification. Yes I solved very quickly, I used myPyllant to make a Flask server to answer to http requests and just an http binding on openhab. A valid alternative, and working very well because I need reports too (gas and energy consumption)
Hello,
yes but it’s really easy, actually I’m upgrading all my env, but if you are confident with Python, you can just run a flask server with your credentials, and query it with openhab, for my system I have this for example, but after updating I will put better:
#!/usr/bin/env python
import asyncio
import logging
from datetime import datetime
from myPyllant.api import MyPyllantAPI
from myPyllant.const import ALL_COUNTRIES, BRANDS, DEFAULT_BRAND
from myPyllant.enums import DeviceDataBucketResolution, ZoneHeatingOperatingMode
# Definisci le tue credenziali e altre variabili qui
username = '<your@email.com>'
password = '<your_password>'
brand = 'vaillant'
country = 'italy' # Ad esempio 'IT' per Italia
# Imposta la verbosità del logging
verbose = False
if verbose:
logging.basicConfig(level=logging.DEBUG)
async def set_zone_mode_off(api, zone):
try:
url = f"{await api.get_system_api_base(zone.system_id)}/zones/{zone.index}/heating-operation-mode"
await api.aiohttp_session.patch(
url,
json={"operationMode": "OFF"},
headers=api.get_authorized_headers(),
)
print(f"Modalità della zona {zone.name} impostata su Riscaldamento spento.")
except Exception as e:
print(f"Errore nell'impostazione della modalità della zona {zone.name}: {e}")
async def main(user, password, brand, country):
async with MyPyllantAPI(user, password, brand, country) as api:
await api.login()
async for system in api.get_systems():
for device in system.devices:
if device.device_type == "BOILER":
start_date = datetime(2024, 5, 1)
end_date = datetime(2024, 5, 31, 23, 59, 59)
async for data in api.get_data_by_device(device, DeviceDataBucketResolution.MONTH, start_date, end_date):
if data.operation_mode == "DOMESTIC_HOT_WATER" and data.energy_type == "CONSUMED_PRIMARY_ENERGY":
for bucket in data.data:
value_m3 = bucket.value / 10000 # Converti il valore in m³
print(f"Consumo di gas per maggio 2024: {value_m3} m³")
break
# Cicla attraverso tutte le zone e stampa le informazioni sulle temperature e sullo stato della modalità di riscaldamento
if system.zones:
for i, zone in enumerate(system.zones):
current_temperature = zone.current_room_temperature
desired_temperature = zone.desired_room_temperature_setpoint
heating_state = zone.heating_state
# Determina la modalità di riscaldamento
operation_mode = zone.heating.operation_mode_heating
if operation_mode == ZoneHeatingOperatingMode.MANUAL:
zone_status = "Manuale"
elif operation_mode == ZoneHeatingOperatingMode.TIME_CONTROLLED:
zone_status = "Temporizzato"
elif operation_mode == ZoneHeatingOperatingMode.OFF:
zone_status = "Riscaldamento spento"
else:
zone_status = "Sconosciuto"
print(f"Zona {zone.name}:")
print(f" Temperatura attuale: {current_temperature}°C")
print(f" Temperatura desiderata: {desired_temperature}°C")
print(f" Stato del riscaldamento: {heating_state}")
print(f" Modalità di riscaldamento: {zone_status}")
if __name__ == "__main__":
asyncio.run(main(username, password, brand, country))
try it, putting some flask method it’s really easy, you can call them from openhab items