Ecowitt support

Update setiing (config_flow and translations) for Ecowitt settings.
Also updated utils.py to accept None for value.
ecowitt_support
SchiZzA 2026-05-27 21:27:13 +02:00
parent 9288ae4a64
commit 211965dd52
No known key found for this signature in database
9 changed files with 1136 additions and 1061 deletions

View File

@ -48,6 +48,7 @@ from .const import (
ECOWITT_ENABLED, ECOWITT_ENABLED,
ECOWITT_URL_PREFIX, ECOWITT_URL_PREFIX,
HEALTH_URL, HEALTH_URL,
LEGACY_ENABLED,
POCASI_CZ_ENABLED, POCASI_CZ_ENABLED,
SENSORS_TO_LOAD, SENSORS_TO_LOAD,
WINDY_ENABLED, WINDY_ENABLED,
@ -421,6 +422,7 @@ def register_path(
_wslink: bool = checked_or(config.options.get(WSLINK), bool, False) _wslink: bool = checked_or(config.options.get(WSLINK), bool, False)
_ecowitt_enabled: bool = checked_or(config.options.get(ECOWITT_ENABLED), bool, False) _ecowitt_enabled: bool = checked_or(config.options.get(ECOWITT_ENABLED), bool, False)
_legacy: bool = checked_or(config.options.get(LEGACY_ENABLED), bool, True)
# Load registred routes # Load registred routes
routes: Routes | None = hass_data.get("routes", None) routes: Routes | None = hass_data.get("routes", None)
@ -450,9 +452,10 @@ def register_path(
raise ConfigEntryNotReady from Ex raise ConfigEntryNotReady from Ex
# Finally create internal route dispatcher with provided urls, while we have webhooks registered. # Finally create internal route dispatcher with provided urls, while we have webhooks registered.
routes.add_route(DEFAULT_URL, _default_route, coordinator.received_data, enabled=not _wslink) routes.add_route(DEFAULT_URL, _default_route, coordinator.received_data, enabled=_legacy and not _wslink)
routes.add_route(WSLINK_URL, _wslink_post_route, coordinator.received_data, enabled=_wslink) routes.add_route(WSLINK_URL, _wslink_post_route, coordinator.received_data, enabled=_legacy and _wslink)
routes.add_route(WSLINK_URL, _wslink_get_route, coordinator.received_data, enabled=_wslink) routes.add_route(WSLINK_URL, _wslink_get_route, coordinator.received_data, enabled=_legacy and _wslink)
# Make health route `sticky` so it will not change upon updating options. # Make health route `sticky` so it will not change upon updating options.
routes.add_route( routes.add_route(
HEALTH_URL, HEALTH_URL,
@ -530,6 +533,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry_data[ENTRY_LAST_OPTIONS] = dict(entry.options) entry_data[ENTRY_LAST_OPTIONS] = dict(entry.options)
_wslink = checked_or(entry.options.get(WSLINK), bool, False) _wslink = checked_or(entry.options.get(WSLINK), bool, False)
_legacy = checked_or(entry.options.get(LEGACY_ENABLED), bool, True)
_ecowitt_enabled = checked_or(entry.options.get(ECOWITT_ENABLED), bool, False) _ecowitt_enabled = checked_or(entry.options.get(ECOWITT_ENABLED), bool, False)
_ecowitt_path = ECOWITT_URL_PREFIX + "/{webhook_id}" _ecowitt_path = ECOWITT_URL_PREFIX + "/{webhook_id}"
@ -537,7 +541,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if routes: if routes:
_LOGGER.debug("We have routes registered, will try to switch dispatcher.") _LOGGER.debug("We have routes registered, will try to switch dispatcher.")
routes.switch_route(coordinator.received_data, DEFAULT_URL if not _wslink else WSLINK_URL) routes.switch_route(coordinator.received_data, DEFAULT_URL if not _wslink else WSLINK_URL, enabled=_legacy)
routes.set_ecowitt_enabled(_ecowitt_path, coordinator.recieved_ecowitt_data, _ecowitt_enabled) routes.set_ecowitt_enabled(_ecowitt_path, coordinator.recieved_ecowitt_data, _ecowitt_enabled)
routes.set_ingress_observer(coordinator_health.record_dispatch) routes.set_ingress_observer(coordinator_health.record_dispatch)
coordinator_health.update_routing(routes) coordinator_health.update_routing(routes)

View File

@ -70,15 +70,17 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.user_data = { self.user_data = {
API_ID: self.config_entry.options.get(API_ID, ""), API_ID: self.config_entry.options.get(API_ID, ""),
API_KEY: self.config_entry.options.get(API_KEY, ""), API_KEY: self.config_entry.options.get(API_KEY, ""),
LEGACY_ENABLED: self.config_entry.options.get(LEGACY_ENABLED, True),
WSLINK: self.config_entry.options.get(WSLINK, False), WSLINK: self.config_entry.options.get(WSLINK, False),
DEV_DBG: self.config_entry.options.get(DEV_DBG, False), DEV_DBG: self.config_entry.options.get(DEV_DBG, False),
} }
self.user_data_schema = { self.user_data_schema = {
vol.Required(API_ID, default=self.user_data.get(API_ID, "")): str, vol.Optional(API_ID, default=self.user_data.get(API_ID, "")): str,
vol.Required(API_KEY, default=self.user_data.get(API_KEY, "")): str, vol.Optional(API_KEY, default=self.user_data.get(API_KEY, "")): str,
vol.Optional(WSLINK, default=self.user_data.get(WSLINK, False)): bool, vol.Optional(WSLINK, default=self.user_data.get(WSLINK, False)): bool,
vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool, vol.Optional(DEV_DBG, default=self.user_data.get(DEV_DBG, False)): bool,
vol.Optional(LEGACY_ENABLED, default=self.user_data.get(LEGACY_ENABLED, True)): bool,
} }
self.sensors = { self.sensors = {
@ -146,7 +148,12 @@ class ConfigOptionsFlowHandler(OptionsFlow):
) )
async def async_step_basic(self, user_input: Any = None): async def async_step_basic(self, user_input: Any = None):
"""Manage basic options - credentials.""" """Manage basic options - PWS/WSLink credentials and legacy endpoint toggle.
API ID/KEY are required only when legacy (PWS/WSLINK) endpoint is enabled.
For an Ecowitt-only setup, the user can turn the legacy endpoint off and leave credantials empty.
"""
errors: dict[str, str] = {} errors: dict[str, str] = {}
await self._get_entry_data() await self._get_entry_data()
@ -158,15 +165,16 @@ class ConfigOptionsFlowHandler(OptionsFlow):
errors=errors, errors=errors,
) )
if user_input[API_ID] in INVALID_CREDENTIALS: if user_input.get(LEGACY_ENABLED):
if user_input[API_ID] in INVALID_CREDENTIALS or user_input.get(API_ID, "") == "":
errors[API_ID] = "valid_credentials_api" errors[API_ID] = "valid_credentials_api"
elif user_input[API_KEY] in INVALID_CREDENTIALS: elif user_input[API_KEY] in INVALID_CREDENTIALS or user_input.get(API_KEY, "") == "":
errors[API_KEY] = "valid_credentials_key" errors[API_KEY] = "valid_credentials_key"
elif user_input[API_KEY] == user_input[API_ID]: elif user_input[API_KEY] == user_input[API_ID]:
errors["base"] = "valid_credentials_match" errors["base"] = "valid_credentials_match"
else:
user_input = self.retain_data(user_input)
if not errors:
user_input = self.retain_data(user_input)
return self.async_create_entry(title=DOMAIN, data=user_input) return self.async_create_entry(title=DOMAIN, data=user_input)
self.user_data = user_input self.user_data = user_input

View File

@ -126,17 +126,19 @@ class Routes:
handler = info.handler if info.enabled else info.fallback handler = info.handler if info.enabled else info.fallback
return await handler(request) return await handler(request)
def switch_route(self, handler: Handler, url_path: str) -> None: def switch_route(self, handler: Handler, url_path: str | None, *, enabled: bool = True) -> None:
"""Enable routes based on URL, disable all others. Leave sticky routes enabled. """Enable routes based on URL, disable all others. Leave sticky routes enabled.
This is called when options change (e.g. WSLink toggle). The aiohttp router stays When `enabled` is False (or url_path is None), all non-sticky (legacy) routes are disabled.
untouched; we only flip which internal handler is active. - used when only Ecowitt is active.
Sticky routes (health, ecowitt) are left untouched.
The aiohttp router stays untouched; we only flip which internal handler is active.
""" """
for route in self.routes.values(): for route in self.routes.values():
if route.sticky: if route.sticky:
continue continue
if route.url_path == url_path: if enabled and route.url_path == url_path:
_LOGGER.info( _LOGGER.info(
"New coordinator to route: (%s):%s", "New coordinator to route: (%s):%s",
route.route.method, route.route.method,

View File

@ -4,9 +4,8 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from homeassistant.components.sensor import SensorEntityDescription
from dev.custom_components.sws12500.const import VOCLevel from dev.custom_components.sws12500.const import VOCLevel
from homeassistant.components.sensor import SensorEntityDescription
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -14,7 +13,7 @@ class WeatherSensorEntityDescription(SensorEntityDescription):
"""Describe Weather Sensor entities.""" """Describe Weather Sensor entities."""
value_fn: Callable[[Any], int | float | str | VOCLevel | None] | None = None value_fn: Callable[[Any], int | float | str | VOCLevel | None] | None = None
value_from_data_fn: Callable[[dict[str, Any]], int | float | str | None] | None = None value_from_data_fn: Callable[[dict[str, Any]], int | float | str | VOCLevel | None] | None = None
deprecated: bool = False deprecated: bool = False
replacement_entity_domain: str | None = None replacement_entity_domain: str | None = None

View File

@ -573,7 +573,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options=list(VOCLevel), options=list(VOCLevel),
icon="mdi:air-filter", icon="mdi:air-filter",
value_from_data_fn=voc_level_to_text(data), value_from_data_fn=lambda data: voc_level_to_text(data.get(VOC, None)),
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=T9_BATTERY, key=T9_BATTERY,

View File

@ -7,19 +7,39 @@
}, },
"step": { "step": {
"user": { "user": {
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant", "title": "Choose your station type",
"title": "Configure access for Weather Station", "description": "Choose the type of your station. If you don't have Eccowit station, choose PWS/WSLink",
"menu_options": {
"pws": "PWS/WSLink (Sencor, Garni, Bresser, other - Weather Underground compatible)",
"ecowitt": "Ecowitt"
}
},
"pws": {
"title": "PWS/WSLink credentials.",
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant.",
"data": { "data": {
"API_ID": "API ID / Station ID", "API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password", "API_KEY": "API KEY / Password",
"WSLINK": "WSLink API", "wslink": "WSLink Protocol",
"dev_debug_checkbox": "Developer log" "dev_debug_checkbox": "Developer log"
}, },
"data_description": { "data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.",
"API_ID": "API ID is the Station ID you set in the Weather Station.", "API_ID": "API ID is the Station ID you set in the Weather Station.",
"API_KEY": "API KEY is the password you set in the Weather Station.", "API_KEY": "API KEY is the password you set in the Weather Station.",
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink." "wslink": "Enable WSLink Protocol if the station is set to send data via WSLink. If you are unsure, use https://test-station.schizza.cz/",
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
}
},
"ecowitt": {
"title": "Ecowitt configuration.",
"description": "No API ID/KEY needed. Set your Ecowitt station to send data to the enndpoint below.",
"data": {
"ecowitt_webhook_id": "Unique webhook ID",
"ecowitt_enabled": "Enable Ecowitt station data"
},
"data_description": {
"ecowitt_webhook_id": "Set your Ecowitt station to send data to the endpoint: {url}:{port}/weatherhub/{webhook_id}",
"ecowitt_enabled": "Enable receiving data from Ecowitt stations"
} }
} }
} }
@ -41,19 +61,19 @@
} }
}, },
"basic": { "basic": {
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant", "description": "Configure the PWS/WSLink endpoint. Turn off 'Enable PWS/WSLink' for an Ecowitt-only setup - API ID/KEY are not required."""title": "Configure PWS/WSLink","data": {
"title": "Configure credentials",
"data": {
"API_ID": "API ID / Station ID", "API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password", "API_KEY": "API KEY / Password",
"WSLINK": "WSLink API", "wslink": "WSLink protocol",
"legacy_enbaled": "Enable PWS/WSLink endpoint (disable for Ecowitt-only setup)",
"dev_debug_checkbox": "Developer log" "dev_debug_checkbox": "Developer log"
}, },
"data_description": { "data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.", "dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.",
"API_ID": "API ID is the Station ID you set in the Weather Station.", "API_ID": "API ID is the Station ID you set in the Weather Station.",
"API_KEY": "API KEY is the password you set in the Weather Station.", "API_KEY": "API KEY is the password you set in the Weather Station.",
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink." "wslink": "Enable WSLink API if the station is set to send data via WSLink. (If you are unsure, use https://test-station.schizza.cz/)",
"legacy_enbaled": "Turn off if your station uses Ecowitt only."
} }
}, },
"windy": { "windy": {

View File

@ -7,19 +7,39 @@
}, },
"step": { "step": {
"user": { "user": {
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem", "title": "Vyberte typ stanice",
"title": "Nastavení přihlášení", "description": "Zadejte typ stanice, kterou používáte. Pokude nepoužíváte Ecowitt, vyberte PWS/WSLink",
"menu_options": {
"pws": "PWS/WSLink (Sencor, Garni, Bresser, jiné - Weather Underground kompatibilní)",
"ecowitt": "Ecowitt"
}
},
"pws": {
"title": "Přihlašovací údaje PWS/WSLink.",
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem.",
"data": { "data": {
"API_ID": "API ID / ID Stanice", "API_ID": "API ID / ID Stanice",
"API_KEY": "API KEY / Heslo", "API_KEY": "API KEY / Heslo",
"wslink": "WSLink API", "wslink": "WSLink Protorkol",
"dev_debug_checkbox": "Developer log" "dev_debug_checkbox": "Developer log"
}, },
"data_description": { "data_description": {
"dev_debug_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři.",
"API_ID": "API ID je ID stanice, které jste nastavili v meteostanici.", "API_ID": "API ID je ID stanice, které jste nastavili v meteostanici.",
"API_KEY": "API KEY je heslo, které jste nastavili v meteostanici.", "API_KEY": "API KEY je heslo, které jste nastavili v meteostanici.",
"wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink." "wslink": "Zapněte tuto volbu, pokud stanice používá WSLink protokol. Pokud si nejstě jistí, použijte https://test-station.schizza.cz/",
"dev_debug_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři."
}
},
"ecowitt": {
"title": "Nastavení pro Ecowitt stanice",
"description": "Zadejte unikátní webhook ID pro příjem dat ze stanic Ecowitt. Pokud nepoužíváte stanice Ecowitt, tento krok přeskočte.",
"data": {
"ecowitt_webhook_id": "Unikátní webhook ID pro Ecowitt stanice",
"ecowitt_enabled": "Povolit data ze stanic Ecowitt"
},
"data_description": {
"ecowitt_webhook_id": "Nastavení pro stanici: {url}:{port}/weatherhub/{webhook_id}",
"ecowitt_enabled": "Povolit přijímání dat ze stanic Ecowitt"
} }
} }
} }
@ -49,19 +69,21 @@
} }
}, },
"basic": { "basic": {
"description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem", "description": "Nastavení endpointu PWS/WSLink. Pro stanici jen s Ecowwittem vypněte 'Povolit endpoint PWS/WSLink'. API ID/KEY nejsou potřeba",
"title": "Nastavení přihlášení", "title": "Nastavení PWS/WSLink",
"data": { "data": {
"API_ID": "API ID / ID Stanice", "API_ID": "API ID / ID Stanice",
"API_KEY": "API KEY / Heslo", "API_KEY": "API KEY / Heslo",
"wslink": "WSLink API", "wslink": "WSLink protokol",
"dev_debug_checkbox": "Developer log" "dev_debug_checkbox": "Developer log",
"legacy_enabled": "Povolit endpoint PWS/WSLink"
}, },
"data_description": { "data_description": {
"dev_debug_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři.", "dev_debug_checkbox": "Zapnout pouze v případě, že chcete poslat ladící informace vývojáři.",
"API_ID": "API ID je ID stanice, které jste nastavili v meteostanici.", "API_ID": "API ID je ID stanice, které jste nastavili v meteostanici.",
"API_KEY": "API KEY je heslo, které jste nastavili v meteostanici.", "API_KEY": "API KEY je heslo, které jste nastavili v meteostanici.",
"wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink." "wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink.",
"legacy_enabled": "Vyplněte, pokud vaše stanice používá pouze Ecowitt."
} }
}, },
"windy": { "windy": {

View File

@ -7,19 +7,39 @@
}, },
"step": { "step": {
"user": { "user": {
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant", "title": "Choose your station type",
"title": "Configure access for Weather Station", "description": "Choose the type of your station. If you don't have Eccowit station, choose PWS/WSLink",
"menu_options": {
"pws": "PWS/WSLink (Sencor, Garni, Bresser, other - Weather Underground compatible)",
"ecowitt": "Ecowitt"
}
},
"pws": {
"title": "PWS/WSLink credentials.",
"description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant.",
"data": { "data": {
"API_ID": "API ID / Station ID", "API_ID": "API ID / Station ID",
"API_KEY": "API KEY / Password", "API_KEY": "API KEY / Password",
"WSLINK": "WSLink API", "wslink": "WSLink Protocol",
"dev_debug_checkbox": "Developer log" "dev_debug_checkbox": "Developer log"
}, },
"data_description": { "data_description": {
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer.",
"API_ID": "API ID is the Station ID you set in the Weather Station.", "API_ID": "API ID is the Station ID you set in the Weather Station.",
"API_KEY": "API KEY is the password you set in the Weather Station.", "API_KEY": "API KEY is the password you set in the Weather Station.",
"WSLINK": "Enable WSLink API if the station is set to send data via WSLink." "wslink": "Enable WSLink Protocol if the station is set to send data via WSLink. If you are unsure, use https://test-station.schizza.cz/",
"dev_debug_checkbox": " Enable only if you want to send debuging data to the developer."
}
},
"ecowitt": {
"title": "Ecowitt configuration.",
"description": "No API ID/KEY needed. Set your Ecowitt station to send data to the enndpoint below.",
"data": {
"ecowitt_webhook_id": "Unique webhook ID",
"ecowitt_enabled": "Enable Ecowitt station data"
},
"data_description": {
"ecowitt_webhook_id": "Set your Ecowitt station to send data to the endpoint: {url}:{port}/weatherhub/{webhook_id}",
"ecowitt_enabled": "Enable receiving data from Ecowitt stations"
} }
} }
} }

View File

@ -372,7 +372,7 @@ def chill_index(data: dict[str, str | float | int], convert: bool = False) -> fl
) )
def voc_level_to_text(value: str) -> VOCLevel | None: def voc_level_to_text(value: str | None) -> VOCLevel | None:
"""Map 1-5 VOC level to text state.""" """Map 1-5 VOC level to text state."""
if value in (None, ""): if value in (None, ""):
return None return None