Compare commits

..

No commits in common. "1ad10b4b1e435ccc1408e270ad19edb747750bb2" and "7abfedc1caadac1a05c7504d17bbb1807e85b9c1" have entirely different histories.

10 changed files with 1107 additions and 1381 deletions

View File

@ -48,7 +48,6 @@ 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,
@ -422,7 +421,6 @@ 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)
@ -452,10 +450,9 @@ 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=_legacy and not _wslink) routes.add_route(DEFAULT_URL, _default_route, coordinator.received_data, enabled=not _wslink)
routes.add_route(WSLINK_URL, _wslink_post_route, coordinator.received_data, enabled=_legacy and _wslink) routes.add_route(WSLINK_URL, _wslink_post_route, coordinator.received_data, enabled=_wslink)
routes.add_route(WSLINK_URL, _wslink_get_route, coordinator.received_data, enabled=_legacy and _wslink) routes.add_route(WSLINK_URL, _wslink_get_route, coordinator.received_data, enabled=_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,
@ -533,7 +530,6 @@ 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}"
@ -541,7 +537,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, enabled=_legacy) routes.switch_route(coordinator.received_data, DEFAULT_URL if not _wslink else WSLINK_URL)
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,7 +19,6 @@ 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,
@ -70,17 +69,15 @@ 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.Optional(API_ID, default=self.user_data.get(API_ID, "")): str, vol.Required(API_ID, default=self.user_data.get(API_ID, "")): str,
vol.Optional(API_KEY, default=self.user_data.get(API_KEY, "")): str, vol.Required(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 = {
@ -148,12 +145,7 @@ 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 - PWS/WSLink credentials and legacy endpoint toggle. """Manage basic options - credentials."""
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()
@ -165,16 +157,15 @@ class ConfigOptionsFlowHandler(OptionsFlow):
errors=errors, errors=errors,
) )
if user_input.get(LEGACY_ENABLED): if user_input[API_ID] in INVALID_CREDENTIALS:
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 or user_input.get(API_KEY, "") == "": elif user_input[API_KEY] in INVALID_CREDENTIALS:
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:
if not errors:
user_input = self.retain_data(user_input) 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
@ -326,7 +317,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."""
pws_schema = { data_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,
@ -337,23 +328,17 @@ 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_menu( return self.async_show_form(
step_id="user", step_id="user",
menu_options=["pws", "ecowitt"], data_schema=vol.Schema(self.data_schema),
) )
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:
@ -361,51 +346,14 @@ 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:
options: dict[str, Any] = { return self.async_create_entry(title=DOMAIN, data=user_input, options=user_input)
**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="pws", step_id="user",
data_schema=vol.Schema(self.pws_schema), data_schema=vol.Schema(self.data_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,7 +145,6 @@ 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"
@ -166,7 +165,6 @@ __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,19 +126,17 @@ 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, *, enabled: bool = True) -> None: def switch_route(self, handler: Handler, url_path: str) -> 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.
When `enabled` is False (or url_path is None), all non-sticky (legacy) routes are disabled. This is called when options change (e.g. WSLink toggle). The aiohttp router stays
- used when only Ecowitt is active. untouched; we only flip which internal handler 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 enabled and route.url_path == url_path: if 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,7 +4,6 @@ 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
@ -12,8 +11,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 | VOCLevel | None] | None = None value_fn: Callable[[Any], int | float | str | None] | None = None
value_from_data_fn: Callable[[dict[str, Any]], int | float | str | VOCLevel | None] | None = None value_from_data_fn: Callable[[dict[str, Any]], int | float | str | 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_5step_to_pct, battery_level, to_float, to_int, voc_level_to_text, wind_dir_to_text from .utils import battery_level, to_float, to_int, 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=to_int, value_fn=lambda data: cast("int", data),
), ),
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=VOC, key=VOC,
@ -538,42 +538,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_fn=voc_level_to_text, value_fn=lambda data: cast("str", voc_level_to_text(data)),
),
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,39 +7,19 @@
}, },
"step": { "step": {
"user": { "user": {
"title": "Choose your station type", "description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"description": "Choose the type of your station. If you don't have Eccowit station, choose PWS/WSLink", "title": "Configure access for Weather Station",
"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 Protocol", "WSLINK": "WSLink API",
"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 Protocol if the station is set to send data via WSLink. If you are unsure, use https://test-station.schizza.cz/", "WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
"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"
} }
} }
} }
@ -61,19 +41,19 @@
} }
}, },
"basic": { "basic": {
"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": { "description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"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 protocol", "WSLINK": "WSLink API",
"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. (If you are unsure, use https://test-station.schizza.cz/)", "WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
"legacy_enbaled": "Turn off if your station uses Ecowitt only."
} }
}, },
"windy": { "windy": {

View File

@ -7,39 +7,19 @@
}, },
"step": { "step": {
"user": { "user": {
"title": "Vyberte typ stanice", "description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
"description": "Zadejte typ stanice, kterou používáte. Pokude nepoužíváte Ecowitt, vyberte PWS/WSLink", "title": "Nastavení přihlášení",
"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 Protorkol", "wslink": "WSLink API",
"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": "Zapněte tuto volbu, pokud stanice používá WSLink protokol. Pokud si nejstě jistí, použijte https://test-station.schizza.cz/", "wslink": "WSLink API zapněte, pokud je stanice nastavena na zasílání dat přes WSLink."
"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"
} }
} }
} }
@ -69,21 +49,19 @@
} }
}, },
"basic": { "basic": {
"description": "Nastavení endpointu PWS/WSLink. Pro stanici jen s Ecowwittem vypněte 'Povolit endpoint PWS/WSLink'. API ID/KEY nejsou potřeba", "description": "Zadejte API ID a API KEY, aby meteostanice mohla komunikovat s HomeAssistantem",
"title": "Nastavení PWS/WSLink", "title": "Nastavení přihlášení",
"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 protokol", "wslink": "WSLink API",
"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,39 +7,19 @@
}, },
"step": { "step": {
"user": { "user": {
"title": "Choose your station type", "description": "Provide API ID and API KEY so the Weather Station can access HomeAssistant",
"description": "Choose the type of your station. If you don't have Eccowit station, choose PWS/WSLink", "title": "Configure access for Weather Station",
"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 Protocol", "WSLINK": "WSLink API",
"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 Protocol if the station is set to send data via WSLink. If you are unsure, use https://test-station.schizza.cz/", "WSLINK": "Enable WSLink API if the station is set to send data via WSLink."
"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,22 +122,23 @@ 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 {REMAP_ITEMS[key]: value for key, value in entities.items() if key in REMAP_ITEMS} return {
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 items in query for WSLink API.""" """Remap WSLink payload field names into internal sensor keys.
items: dict[str, str] = {}
for item, value in entities.items():
if item in REMAP_WSLINK_ITEMS:
items[REMAP_WSLINK_ITEMS[item]] = value
for conn_key, gated in CONNECTION_GATED_SENSORS.items(): WSLink uses a different naming scheme than the legacy endpoint (e.g. "t1tem", "t1ws").
if str(entities.get(conn_key, "0")) != "1": Just like `remap_items`, this function normalizes the payload to the integration's stable
for key in gated: internal keys.
items.pop(key, None) """
return {
return items REMAP_WSLINK_ITEMS[key]: value
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]:
@ -149,7 +150,9 @@ 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(items: dict[str, str], config_entry: ConfigEntry) -> list[str] | None: def check_disabled(
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
@ -287,7 +290,9 @@ def to_float(val: Any) -> float | None:
return v return v
def heat_index(data: dict[str, int | float | str], convert: bool = False) -> float | None: def heat_index(
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
@ -336,7 +341,9 @@ def heat_index(data: dict[str, int | float | str], convert: bool = False) -> flo
return simple return simple
def chill_index(data: dict[str, str | float | int], convert: bool = False) -> float | None: def chill_index(
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
@ -370,126 +377,3 @@ def chill_index(data: dict[str, str | float | int], convert: bool = False) -> fl
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