Compare commits

..

4 Commits

Author SHA1 Message Date
SchiZzA 1ad10b4b1e
fixed: Menu options 2026-05-27 21:34:32 +02:00
SchiZzA b9815713a0
fix value in wslink sensors
Fix VOCLevel value
2026-05-27 21:30:11 +02:00
SchiZzA 211965dd52
Ecowitt support
Update setiing (config_flow and translations) for Ecowitt settings.
Also updated utils.py to accept None for value.
2026-05-27 21:27:13 +02:00
SchiZzA 9288ae4a64
Updated config flow
Updated config flow's initial setup.
Ecowitt stations does not require `API_ID` and `API_KEY`, so we need to
separate Ecowitt setup from PWS/WSLink setup.
2026-05-27 17:27:10 +02:00
10 changed files with 1369 additions and 1095 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

@ -19,6 +19,7 @@ from .const import (
ECOWITT_ENABLED, ECOWITT_ENABLED,
ECOWITT_WEBHOOK_ID, ECOWITT_WEBHOOK_ID,
INVALID_CREDENTIALS, INVALID_CREDENTIALS,
LEGACY_ENABLED,
POCASI_CZ_API_ID, POCASI_CZ_API_ID,
POCASI_CZ_API_KEY, POCASI_CZ_API_KEY,
POCASI_CZ_ENABLED, POCASI_CZ_ENABLED,
@ -69,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 = {
@ -145,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()
@ -157,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
@ -317,7 +326,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
class ConfigFlowHandler(ConfigFlow, domain=DOMAIN): class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Sencor SWS 12500 Weather Station.""" """Handle a config flow for Sencor SWS 12500 Weather Station."""
data_schema = { pws_schema = {
vol.Required(API_ID): str, vol.Required(API_ID): str,
vol.Required(API_KEY): str, vol.Required(API_KEY): str,
vol.Optional(WSLINK): bool, vol.Optional(WSLINK): bool,
@ -328,17 +337,23 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_user(self, user_input: Any = None): async def async_step_user(self, user_input: Any = None):
"""Handle the initial step.""" """Handle the initial step."""
if user_input is None:
await self.async_set_unique_id(DOMAIN) await self.async_set_unique_id(DOMAIN)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_show_form( return self.async_show_menu(
step_id="user", step_id="user",
data_schema=vol.Schema(self.data_schema), menu_options=["pws", "ecowitt"],
) )
async def async_step_pws(self, user_input: Any = None) -> ConfigFlowResult:
"""PWS/WSLink credentials setup."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is None:
return self.async_show_form(step_id="pws", data_schema=vol.Schema(self.pws_schema), errors=errors)
if user_input[API_ID] in INVALID_CREDENTIALS: if user_input[API_ID] in INVALID_CREDENTIALS:
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:
@ -346,14 +361,51 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
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: else:
return self.async_create_entry(title=DOMAIN, data=user_input, options=user_input) options: dict[str, Any] = {
**user_input,
LEGACY_ENABLED: True,
ECOWITT_ENABLED: False,
}
return self.async_create_entry(title=DOMAIN, data=options, options=options)
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="pws",
data_schema=vol.Schema(self.data_schema), data_schema=vol.Schema(self.pws_schema),
errors=errors, errors=errors,
) )
async def async_step_ecowitt(self, user_input: Any = None) -> ConfigFlowResult:
"""Ecowitt stations setup."""
if user_input is None:
webhook = secrets.token_hex(8)
url: URL = URL(get_url(self.hass))
host = url.host or "UNKNOWN"
ecowitt_schema = {
vol.Required(ECOWITT_WEBHOOK_ID, default=webhook): str,
vol.Optional(ECOWITT_ENABLED, default=True): bool,
}
return self.async_show_form(
step_id="ecowitt",
data_schema=vol.Schema(ecowitt_schema),
description_placeholders={
"url": host,
"port": str(url.port),
"webhook_id": webhook,
},
)
options: dict[str, Any] = {
**user_input,
LEGACY_ENABLED: False,
WSLINK: False,
API_ID: "",
API_KEY: "",
}
return self.async_create_entry(title=DOMAIN, data=options, options=options)
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry: ConfigEntry) -> ConfigOptionsFlowHandler: def async_get_options_flow(config_entry: ConfigEntry) -> ConfigOptionsFlowHandler:

View File

@ -145,6 +145,7 @@ POCASI_CZ_SEND_MINIMUM: Final = 12 # minimal time to resend data
WSLINK: Final = "wslink" WSLINK: Final = "wslink"
LEGACY_ENABLED: Final = "legacy_enabled"
WINDY_MAX_RETRIES: Final = 3 WINDY_MAX_RETRIES: Final = 3
WSLINK_ADDON_PORT: Final = "WSLINK_ADDON_PORT" WSLINK_ADDON_PORT: Final = "WSLINK_ADDON_PORT"
@ -165,6 +166,7 @@ __all__ = [
"SENSOR_TO_MIGRATE", "SENSOR_TO_MIGRATE",
"DEV_DBG", "DEV_DBG",
"WSLINK", "WSLINK",
"LEGACY_ENABLED",
"ECOWITT", "ECOWITT",
"ECOWITT_WEBHOOK_ID", "ECOWITT_WEBHOOK_ID",
"ECOWITT_ENABLED", "ECOWITT_ENABLED",

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,6 +4,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from dev.custom_components.sws12500.const import VOCLevel
from homeassistant.components.sensor import SensorEntityDescription from homeassistant.components.sensor import SensorEntityDescription
@ -11,8 +12,8 @@ from homeassistant.components.sensor import SensorEntityDescription
class WeatherSensorEntityDescription(SensorEntityDescription): class WeatherSensorEntityDescription(SensorEntityDescription):
"""Describe Weather Sensor entities.""" """Describe Weather Sensor entities."""
value_fn: Callable[[Any], int | float | str | 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

@ -67,7 +67,7 @@ from .const import (
VOCLevel, VOCLevel,
) )
from .sensors_common import WeatherSensorEntityDescription from .sensors_common import WeatherSensorEntityDescription
from .utils import battery_level, to_float, to_int, wind_dir_to_text from .utils import battery_5step_to_pct, battery_level, to_float, to_int, voc_level_to_text, wind_dir_to_text
SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
@ -530,7 +530,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
icon="mdi:molecule", icon="mdi:molecule",
value_fn=lambda data: cast("int", data), value_fn=to_int,
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=VOC, key=VOC,
@ -538,7 +538,42 @@ 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_fn=lambda data: cast("str", voc_level_to_text(data)), value_fn=voc_level_to_text,
),
WeatherSensorEntityDescription(
key=T9_BATTERY,
translation_key=T9_BATTERY,
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
value_fn=battery_5step_to_pct,
),
WeatherSensorEntityDescription(
key=HCHO,
translation_key=HCHO,
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:molecule",
value_fn=to_int,
),
WeatherSensorEntityDescription(
key=HCHO,
translation_key=HCHO,
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:molecule",
value_fn=to_int,
),
WeatherSensorEntityDescription(
key=VOC,
translation_key=VOC,
device_class=SensorDeviceClass.ENUM,
options=list(VOCLevel),
icon="mdi:air-filter",
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

@ -25,7 +25,7 @@ from homeassistant.helpers.translation import async_get_translations
from .const import ( from .const import (
AZIMUT, AZIMUT,
CONNECTION_GATED_SENSORS, CONNECTION_GATED_SENSORS,
DATABASE_PATH, # DATABASE_PATH,
DEV_DBG, DEV_DBG,
OUTSIDE_HUMIDITY, OUTSIDE_HUMIDITY,
OUTSIDE_TEMP, OUTSIDE_TEMP,
@ -122,23 +122,22 @@ def remap_items(entities: dict[str, str]) -> dict[str, str]:
stable keys from `const.py` (e.g. "outside_temp", "outside_humidity"). This function produces stable keys from `const.py` (e.g. "outside_temp", "outside_humidity"). This function produces
a normalized dict that the rest of the integration can work with. a normalized dict that the rest of the integration can work with.
""" """
return { return {REMAP_ITEMS[key]: value for key, value in entities.items() if key in REMAP_ITEMS}
REMAP_ITEMS[key]: value for key, value in entities.items() if key in REMAP_ITEMS
}
def remap_wslink_items(entities: dict[str, str]) -> dict[str, str]: def remap_wslink_items(entities: dict[str, str]) -> dict[str, str]:
"""Remap WSLink payload field names into internal sensor keys. """Remap items in query for WSLink API."""
items: dict[str, str] = {}
for item, value in entities.items():
if item in REMAP_WSLINK_ITEMS:
items[REMAP_WSLINK_ITEMS[item]] = value
WSLink uses a different naming scheme than the legacy endpoint (e.g. "t1tem", "t1ws"). for conn_key, gated in CONNECTION_GATED_SENSORS.items():
Just like `remap_items`, this function normalizes the payload to the integration's stable if str(entities.get(conn_key, "0")) != "1":
internal keys. for key in gated:
""" items.pop(key, None)
return {
REMAP_WSLINK_ITEMS[key]: value return items
for key, value in entities.items()
if key in REMAP_WSLINK_ITEMS
}
def loaded_sensors(config_entry: ConfigEntry) -> list[str]: def loaded_sensors(config_entry: ConfigEntry) -> list[str]:
@ -150,9 +149,7 @@ def loaded_sensors(config_entry: ConfigEntry) -> list[str]:
return config_entry.options.get(SENSORS_TO_LOAD) or [] return config_entry.options.get(SENSORS_TO_LOAD) or []
def check_disabled( def check_disabled(items: dict[str, str], config_entry: ConfigEntry) -> list[str] | None:
items: dict[str, str], config_entry: ConfigEntry
) -> list[str] | None:
"""Detect payload fields that are not enabled yet (auto-discovery). """Detect payload fields that are not enabled yet (auto-discovery).
The integration supports "auto-discovery" of sensors: when the station starts sending a new The integration supports "auto-discovery" of sensors: when the station starts sending a new
@ -290,9 +287,7 @@ def to_float(val: Any) -> float | None:
return v return v
def heat_index( def heat_index(data: dict[str, int | float | str], convert: bool = False) -> float | None:
data: dict[str, int | float | str], convert: bool = False
) -> float | None:
"""Calculate heat index from temperature. """Calculate heat index from temperature.
data: dict with temperature and humidity data: dict with temperature and humidity
@ -341,9 +336,7 @@ def heat_index(
return simple return simple
def chill_index( def chill_index(data: dict[str, str | float | int], convert: bool = False) -> float | None:
data: dict[str, str | float | int], convert: bool = False
) -> float | None:
"""Calculate wind chill index from temperature and wind speed. """Calculate wind chill index from temperature and wind speed.
data: dict with temperature and wind speed data: dict with temperature and wind speed
@ -377,3 +370,126 @@ def chill_index(
if temp < 50 and wind > 3 if temp < 50 and wind > 3
else temp else temp
) )
def voc_level_to_text(value: str | None) -> VOCLevel | None:
"""Map 1-5 VOC level to text state."""
if value in (None, ""):
return None
return VOC_LEVEL_MAP.get(int(value))
def battery_5step_to_pct(value: str) -> int | None:
"""Convert 0-5 battery steps to percentage."""
if value in (None, ""):
return None
return round(int(value) / 5 * 100)
#
# def long_term_units_in_statistics_meta():
# """Get units in long term statitstics."""
# sensor_units = []
# if not Path(DATABASE_PATH).exists():
# _LOGGER.error("Database file not found: %s", DATABASE_PATH)
# return False
#
# conn = sqlite3.connect(DATABASE_PATH)
# db = conn.cursor()
#
# try:
# db.execute(
# """
# SELECT statistic_id, unit_of_measurement from statistics_meta
# WHERE statistic_id LIKE 'sensor.weather_station_sws%'
# """
# )
# rows = db.fetchall()
# sensor_units = {
# statistic_id: f"{statistic_id} ({unit})" for statistic_id, unit in rows
# }
#
# except sqlite3.Error as e:
# _LOGGER.error("Error during data migration: %s", e)
# finally:
# conn.close()
#
# return sensor_units
#
#
# async def migrate_data(hass: HomeAssistant, sensor_id: str | None = None) -> int | bool:
# """Migrate data from mm/d to mm."""
#
# _LOGGER.debug("Sensor %s is required for data migration", sensor_id)
# updated_rows = 0
#
# if not Path(DATABASE_PATH).exists():
# _LOGGER.error("Database file not found: %s", DATABASE_PATH)
# return False
#
# conn = sqlite3.connect(DATABASE_PATH)
# db = conn.cursor()
#
# try:
# _LOGGER.info(sensor_id)
# db.execute(
# """
# UPDATE statistics_meta
# SET unit_of_measurement = 'mm'
# WHERE statistic_id = ?
# AND unit_of_measurement = 'mm/d';
# """,
# (sensor_id,),
# )
# updated_rows = db.rowcount
# conn.commit()
# _LOGGER.info(
# "Data migration completed successfully. Updated rows: %s for %s",
# updated_rows,
# sensor_id,
# )
#
# except sqlite3.Error as e:
# _LOGGER.error("Error during data migration: %s", e)
# finally:
# conn.close()
# return updated_rows
#
#
# def migrate_data_old(sensor_id: str | None = None):
# """Migrate data from mm/d to mm."""
# updated_rows = 0
#
# if not Path(DATABASE_PATH).exists():
# _LOGGER.error("Database file not found: %s", DATABASE_PATH)
# return False
#
# conn = sqlite3.connect(DATABASE_PATH)
# db = conn.cursor()
#
# try:
# _LOGGER.info(sensor_id)
# db.execute(
# """
# UPDATE statistics_meta
# SET unit_of_measurement = 'mm'
# WHERE statistic_id = ?
# AND unit_of_measurement = 'mm/d';
# """,
# (sensor_id,),
# )
# updated_rows = db.rowcount
# conn.commit()
# _LOGGER.info(
# "Data migration completed successfully. Updated rows: %s for %s",
# updated_rows,
# sensor_id,
# )
#
# except sqlite3.Error as e:
# _LOGGER.error("Error during data migration: %s", e)
# finally:
# conn.close()
# return updated_rows