Update Windy integration to use station ID and password authentication

- Replace API key with station ID and password for authentication
- Change Windy API endpoint to v2 observation update
- Adapt data conversion for WSLink to Windy format
- Update config flow and translations accordingly
ecowitt_support
SchiZzA 2026-02-06 15:16:05 +01:00
parent 176420aa43
commit 0c67b8d2ab
No known key found for this signature in database
6 changed files with 78 additions and 22 deletions

View File

@ -161,7 +161,7 @@ class WeatherDataUpdateCoordinator(DataUpdateCoordinator):
# Optional forwarding to external services. This is kept here (in the webhook handler) # Optional forwarding to external services. This is kept here (in the webhook handler)
# to avoid additional background polling tasks. # to avoid additional background polling tasks.
if self.config.options.get(WINDY_ENABLED, False): if self.config.options.get(WINDY_ENABLED, False):
await self.windy.push_data_to_windy(data) await self.windy.push_data_to_windy(data, _wslink)
if self.config.options.get(POCASI_CZ_ENABLED, False): if self.config.options.get(POCASI_CZ_ENABLED, False):
await self.pocasi.push_data_to_server(data, "WSLINK" if _wslink else "WU") await self.pocasi.push_data_to_server(data, "WSLINK" if _wslink else "WU")

View File

@ -31,9 +31,10 @@ from .const import (
POCASI_CZ_SEND_INTERVAL, POCASI_CZ_SEND_INTERVAL,
POCASI_CZ_SEND_MINIMUM, POCASI_CZ_SEND_MINIMUM,
SENSORS_TO_LOAD, SENSORS_TO_LOAD,
WINDY_API_KEY,
WINDY_ENABLED, WINDY_ENABLED,
WINDY_LOGGER_ENABLED, WINDY_LOGGER_ENABLED,
WINDY_STATION_ID,
WINDY_STATION_PW,
WSLINK, WSLINK,
) )
@ -94,8 +95,8 @@ class ConfigOptionsFlowHandler(OptionsFlow):
} }
self.windy_data = { self.windy_data = {
WINDY_API_KEY: self.config_entry.options.get(WINDY_API_KEY), WINDY_STATION_ID: self.config_entry.options.get(WINDY_STATION_ID, ""),
WINDY_ENABLED: self.config_entry.options.get(WINDY_ENABLED, False), WINDY_STATION_PW: self.config_entry.options.get(WINDY_STATION_PW, ""),
WINDY_LOGGER_ENABLED: self.config_entry.options.get( WINDY_LOGGER_ENABLED: self.config_entry.options.get(
WINDY_LOGGER_ENABLED, False WINDY_LOGGER_ENABLED, False
), ),
@ -103,7 +104,11 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.windy_data_schema = { self.windy_data_schema = {
vol.Optional( vol.Optional(
WINDY_API_KEY, default=self.windy_data.get(WINDY_API_KEY, "") WINDY_STATION_ID, default=self.windy_data.get(WINDY_STATION_ID, "")
): str,
vol.Optional(
WINDY_STATION_PW,
default=self.windy_data.get(WINDY_STATION_PW, ""),
): str, ): str,
vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool vol.Optional(WINDY_ENABLED, default=self.windy_data[WINDY_ENABLED]): bool
or False, or False,
@ -202,8 +207,10 @@ class ConfigOptionsFlowHandler(OptionsFlow):
errors=errors, errors=errors,
) )
if (user_input[WINDY_ENABLED] is True) and (user_input[WINDY_API_KEY] == ""): if (user_input[WINDY_ENABLED] is True) and (
errors[WINDY_API_KEY] = "windy_key_required" (user_input[WINDY_STATION_ID] == "") or (user_input[WINDY_STATION_PW] == "")
):
errors[WINDY_STATION_ID] = "windy_key_required"
return self.async_show_form( return self.async_show_form(
step_id="windy", step_id="windy",
data_schema=vol.Schema(self.windy_data_schema), data_schema=vol.Schema(self.windy_data_schema),

View File

@ -6,7 +6,7 @@ from typing import Final
DOMAIN = "sws12500" DOMAIN = "sws12500"
DEFAULT_URL = "/weatherstation/updateweatherstation.php" DEFAULT_URL = "/weatherstation/updateweatherstation.php"
WSLINK_URL = "/data/upload.php" WSLINK_URL = "/data/upload.php"
WINDY_URL = "https://stations.windy.com/pws/update/" WINDY_URL = "https://stations.windy.com/api/v2/observation/update"
DATABASE_PATH = "/config/home-assistant_v2.db" DATABASE_PATH = "/config/home-assistant_v2.db"
POCASI_CZ_URL: Final = "http://ms.pocasimeteo.cz" POCASI_CZ_URL: Final = "http://ms.pocasimeteo.cz"
@ -41,7 +41,8 @@ POCASI_CZ_UNEXPECTED: Final = (
"Pocasti Meteo responded unexpectedly 3 times in row. Resendig is now disabled!" "Pocasti Meteo responded unexpectedly 3 times in row. Resendig is now disabled!"
) )
WINDY_API_KEY = "WINDY_API_KEY" WINDY_STATION_ID = "WINDY_STATION_ID"
WINDY_STATION_PW = "WINDY_STATION_PWD"
WINDY_ENABLED: Final = "windy_enabled_checkbox" WINDY_ENABLED: Final = "windy_enabled_checkbox"
WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox" WINDY_LOGGER_ENABLED: Final = "windy_logger_checkbox"
WINDY_NOT_INSERTED: Final = "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?" WINDY_NOT_INSERTED: Final = "Data was succefuly sent to Windy, but not inserted by Windy API. Does anyone else sent data to Windy?"

View File

@ -66,12 +66,14 @@
"description": "Přeposílání dat z metostanice na Windy", "description": "Přeposílání dat z metostanice na Windy",
"title": "Konfigurace Windy", "title": "Konfigurace Windy",
"data": { "data": {
"WINDY_API_KEY": "Klíč API KEY získaný z Windy", "WINDY_STATION_ID": "ID stanice, získaný z Windy",
"WINDY_STATION_PWD": "Heslo stanice, získané z Windy",
"windy_enabled_checkbox": "Povolit přeposílání dat na Windy", "windy_enabled_checkbox": "Povolit přeposílání dat na Windy",
"windy_logger_checkbox": "Logovat data a odpovědi z Windy" "windy_logger_checkbox": "Logovat data a odpovědi z Windy"
}, },
"data_description": { "data_description": {
"WINDY_API_KEY": "Klíč API KEY získaný z https://api.windy.com/keys", "WINDY_STATION_ID": "ID stanice získaný z https://stations.windy.com/station",
"WINDY_STATION_PWD": "Heslo stanice získané z https://stations.windy.com/station",
"windy_logger_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři." "windy_logger_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři."
} }
}, },

View File

@ -60,12 +60,14 @@
"description": "Resend weather data to your Windy stations.", "description": "Resend weather data to your Windy stations.",
"title": "Configure Windy", "title": "Configure Windy",
"data": { "data": {
"WINDY_API_KEY": "API KEY provided by Windy", "WINDY_STATION_ID": "Station ID obtained form Windy",
"WINDY_STATION_PWD": "Station password obtained from Windy",
"windy_enabled_checkbox": "Enable resending data to Windy", "windy_enabled_checkbox": "Enable resending data to Windy",
"windy_logger_checkbox": "Log Windy data and responses" "windy_logger_checkbox": "Log Windy data and responses"
}, },
"data_description": { "data_description": {
"WINDY_API_KEY": "Windy API KEY obtained from https://https://api.windy.com/keys", "WINDY_STATION_ID": "Windy station ID obtained from https://stations.windy.com/stations",
"WINDY_STATION_PWD": "Windy station password obtained from https://stations.windy.com/stations",
"windy_logger_checkbox": "Enable only if you want to send debuging data to the developer." "windy_logger_checkbox": "Enable only if you want to send debuging data to the developer."
} }
}, },

View File

@ -4,7 +4,7 @@ from datetime import datetime, timedelta
import logging import logging
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
from py_typecheck.core import checked from py_typecheck import checked
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -12,11 +12,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ( from .const import (
PURGE_DATA, PURGE_DATA,
WINDY_API_KEY,
WINDY_ENABLED, WINDY_ENABLED,
WINDY_INVALID_KEY, WINDY_INVALID_KEY,
WINDY_LOGGER_ENABLED, WINDY_LOGGER_ENABLED,
WINDY_NOT_INSERTED, WINDY_NOT_INSERTED,
WINDY_STATION_ID,
WINDY_STATION_PW,
WINDY_SUCCESS, WINDY_SUCCESS,
WINDY_UNEXPECTED, WINDY_UNEXPECTED,
WINDY_URL, WINDY_URL,
@ -84,7 +85,34 @@ class WindyPush:
if "Unauthorized" in response: if "Unauthorized" in response:
raise WindyApiKeyError raise WindyApiKeyError
async def push_data_to_windy(self, data: dict[str, str]) -> bool: def _covert_wslink_to_pws(self, indata: dict[str, str]) -> dict[str, str]:
"""Convert WSLink API data to Windy API data protocol."""
if "t1ws" in indata:
indata["wind"] = indata.pop("t1ws")
if "t1wgust" in indata:
indata["gust"] = indata.pop("t1wgust")
if "t1wdir" in indata:
indata["winddir"] = indata.pop("t1wdir")
if "t1hum" in indata:
indata["humidity"] = indata.pop("t1hum")
if "t1dew" in indata:
indata["dewpoint"] = indata.pop("t1dew")
if "t1tem" in indata:
indata["temp"] = indata.pop("t1tem")
if "rbar" in indata:
indata["mbar"] = indata.pop("rbar")
if "t1rainhr" in indata:
indata["precip"] = indata.pop("t1rainhr")
if "t1uvi" in indata:
indata["uv"] = indata.pop("t1uvi")
if "t1solrad" in indata:
indata["solarradiation"] = indata.pop("t1solrad")
return indata
async def push_data_to_windy(
self, data: dict[str, str], wslink: bool = False
) -> bool:
"""Pushes weather data do Windy stations. """Pushes weather data do Windy stations.
Interval is 5 minutes, otherwise Windy would not accepts data. Interval is 5 minutes, otherwise Windy would not accepts data.
@ -109,23 +137,39 @@ class WindyPush:
if purge in purged_data: if purge in purged_data:
_ = purged_data.pop(purge) _ = purged_data.pop(purge)
if "dewptf" in purged_data: if wslink:
dewpoint = round(((float(purged_data.pop("dewptf")) - 32) / 1.8), 1) # WSLink -> Windy params
purged_data["dewpoint"] = str(dewpoint) self._covert_wslink_to_pws(purged_data)
if ( if (
windy_api_key := checked(self.config.options.get(WINDY_API_KEY), str) windy_station_id := checked(self.config.options.get(WINDY_STATION_ID), str)
) is None: ) is None:
_LOGGER.error("Windy API key is not provided! Check your configuration.") _LOGGER.error("Windy API key is not provided! Check your configuration.")
return False return False
request_url = f"{WINDY_URL}{windy_api_key}" if (
windy_station_pw := checked(self.config.options.get(WINDY_STATION_PW), str)
) is None:
_LOGGER.error(
"Windy station password is missing! Check your configuration."
)
return False
request_url = f"{WINDY_URL}"
purged_data["id"] = windy_station_id
purged_data["time"] = "now"
headers = {"Authorization": f"Bearer {windy_station_pw}"}
if self.log: if self.log:
_LOGGER.info("Dataset for windy: %s", purged_data) _LOGGER.info("Dataset for windy: %s", purged_data)
session = async_get_clientsession(self.hass, verify_ssl=False) session = async_get_clientsession(self.hass, verify_ssl=False)
try: try:
async with session.get(request_url, params=purged_data) as resp: async with session.get(
request_url, params=purged_data, headers=headers
) as resp:
status = await resp.text() status = await resp.text()
try: try:
self.verify_windy_response(status) self.verify_windy_response(status)