From 6914d87f5c359dbb4589a5be622e7a594325fed9 Mon Sep 17 00:00:00 2001 From: SchiZzA Date: Sat, 25 Jul 2026 22:37:28 +0200 Subject: [PATCH] no-mistakes(document): Update README for Ecowitt, Windy and diagnostics changes --- README.md | 109 +++++++++++++++++++++++++++++++++++------- tests/test_strings.py | 85 ++++++++++++++++++++++++++++++-- 2 files changed, 173 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 62246b3..c4a00ae 100644 --- a/README.md +++ b/README.md @@ -5,24 +5,26 @@ This integration will listen for data from your station and passes them to respective sensors. It also provides the ability to push data to `Windy API` or `Pocasi Meteo`. -### Ecowitt support is coming in the next major release +### Ecowitt support -As of April 11, 2026, Ecowitt stations are supported in the pre-release version +Ecowitt stations are supported as of the pre-release version [v2.0.0pre1](https://github.com/schizza/SWS-12500-custom-component/releases/tag/v2.0.0pre1). You can download this pre-release in HACS under `target version`, where you can pick the exact version of the integration. Please be aware that this pre-release is really for testing purposes only. +See [Ecowitt stations](#ecowitt-stations) for the setup steps. + --- ### Integration rename is planned for the next major release The current name no longer reflects what the integration does. It was initially developed primarily for the SWS 12500 station, but it already supports other weather stations as well -(Bresser, Garni and others), and Ecowitt support is on the way — so the current name has -become misleading. The transition will be announced via an update, and I'm also planning to -offer a full data migration from the existing integration to the new one, so you won't lose -any of your historical data. +(Bresser, Garni, Ecowitt and others) — so the current name has become misleading. The +transition will be announced via an update, and I'm also planning to offer a full data +migration from the existing integration to the new one, so you won't lose any of your +historical data. - The transition date hasn't been set yet, but it's currently expected to happen within the next ~2–3 months. At the moment, I'm working on a full refactor and general code cleanup. @@ -95,6 +97,8 @@ Web server repo is reachable here: `Devices & services` find SWS12500 and click `Configure`. -- In dialog box choose `Basic - Configure credentials` +- In dialog box choose `Basic - configure credentials for Weather Station` ![reconfigure dialog](README/reconfigure.png) As soon as the integration is added into Home Assistant it will listen for incoming data from the station and starts to fill sensors as soon as data will first arrive. +## Ecowitt stations + +Ecowitt stations do not use the `API ID` / `API KEY` pair — they are authorized by a +secret webhook ID that the integration generates for you. + +- When adding the integration, pick `Ecowitt` in the station-type dialog. For an entry + that already exists, go to `Settings` -> `Devices & services` -> SWS12500 -> + `Configure` and choose `Ecowitt configuration`. +- The dialog shows the endpoint your station has to post to and pre-fills a freshly + generated webhook ID: + +```plain +:/weatherhub/ +``` + +- Enter that path as the custom-server path in the Ecowitt app (`WS View` / `Customized` + upload), keep the protocol on `Ecowitt`, and tick `Enable Ecowitt station data`. + +> [!IMPORTANT] +> The legacy PWS/WSLink endpoint and the Ecowitt endpoint cannot be enabled at the same +> time — both feed the same sensor entities, which would mix up units and blank out +> readings. The config flow refuses to enable one while the other is on. Picking +> `Ecowitt` when you first add the integration turns the legacy endpoint off for you; to +> switch back, disable Ecowitt first and then tick `Enable PWS/WSLink endpoint` under +> `Basic - configure credentials for Weather Station`. + ## Upgrading from PWS to WSLink If you upgrade your station — which was previously sending data in the PWS protocol — to a @@ -190,19 +239,26 @@ measurement scale. ## Resending data to Windy API - First of all you need to create account at [Windy stations](https://stations.windy.com). -- Once you have an account created, copy your Windy API Key. - ![windy api key](README/windy_key.png) +- Once you have an account created, open + [your stations](https://stations.windy.com/stations) and copy the **station ID** and the + **station password** of the station you want to feed. + ![windy station credentials](README/windy_key.png) - In `Settings` -> `Devices & services` find SWS12500 and click `Configure`. - In dialog box choose `Windy configuration`. ![config dialog](README/cfg.png) -- Fill in `Key` you were provided at `Windy stations`. -- Tick `Enable` checkbox. +- Fill in `Station ID obtained form Windy` and `Station password obtained from Windy`. + Both are required — enabling the forwarder with either one empty is rejected. +- Tick `Enable resending data to Windy`. ![enable windy](README/windy_cfg.png) - You are done. +If Windy rejects the data three times in a row, the integration disables resending on its +own, writes the reason to the log and raises a persistent notification — fix the +credentials and tick `Enable resending data to Windy` again. + ## Resending data to Pocasi Meteo - If you are willing to use [Pocasi Meteo Application](https://pocasimeteo.cz) you can enable resending your data to their servers @@ -210,10 +266,15 @@ measurement scale. - In `Settings` -> `Devices & services` find SWS12500 and click `Configure`. - In dialog box choose `Pocasi Meteo configuration`. - Fill in `ID` and `KEY` you were provided at `Pocasi Meteo`. -- Tick `Enable` checkbox. +- Optionally adjust `Resend interval in seconds` (minimum 12s, default 30s). +- Tick `Enable resending data to Pocasi Meteo`. - You are done. +As with Windy, three unexpected responses in a row switch resending off automatically and +log the reason. The `Forwarding to Počasí Meteo` and `Forwarding status to Počasí Meteo` +diagnostic sensors show the current state. + ## WSLink notes If your station sends WSLink data over SSL (see the @@ -239,3 +300,17 @@ WSLink proxy add-on is listening on port 4443 - Your station will send data to the SSL proxy and the add-on will handle the rest. _Most stations do not care about self-signed certificates on the server side._ + +### Add-on health monitoring + +The integration probes the add-on's health endpoint so the diagnostic entities can report +whether the proxy is online, which version it runs and which ports it uses. It has to know +the port to reach it on: + +- In `Settings` -> `Devices & services` find SWS12500, click `Configure` and choose + `WSLink add-on port`. +- Set it to the same port the add-on listens on — the one you pointed the station at + above. The integration defaults to `443`, so if your add-on listens elsewhere (`4443` + in the example above), change it here. + +This setting only affects the health probe; incoming station data is unaffected by it. diff --git a/tests/test_strings.py b/tests/test_strings.py index 1625286..23b58b6 100644 --- a/tests/test_strings.py +++ b/tests/test_strings.py @@ -1,6 +1,83 @@ -# Test file for strings.json module +"""Tests for the user-facing strings shipped with the integration. -def test_strings_functionality(): - # Add your test cases here - pass +`strings.json` is the source of truth; `translations/en.json` and `translations/cs.json` +are what the frontend actually loads. A key that exists in one file and not in another +(or a state key that no entity ever emits) is invisible in code review and only shows up +as an untranslated raw value in the UI. +""" +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest + +from custom_components.sws12500.sensors_weather import SENSOR_TYPES_WEATHER_API +from custom_components.sws12500.sensors_wslink import SENSOR_TYPES_WSLINK +from homeassistant.components.sensor import SensorDeviceClass + +COMPONENT_DIR = Path(__file__).resolve().parents[1] / "custom_components" / "sws12500" + +STRINGS_FILE = COMPONENT_DIR / "strings.json" +TRANSLATION_FILES = ( + COMPONENT_DIR / "translations" / "en.json", + COMPONENT_DIR / "translations" / "cs.json", +) + + +def _load(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def _flatten(data: dict[str, Any], prefix: str = "") -> set[str]: + """Return every leaf path in a translation document.""" + keys: set[str] = set() + for key, value in data.items(): + path = f"{prefix}.{key}" if prefix else key + if isinstance(value, dict): + keys |= _flatten(value, path) + else: + keys.add(path) + return keys + + +@pytest.mark.parametrize("translation_file", TRANSLATION_FILES, ids=lambda p: p.name) +def test_translation_keys_match_strings_json(translation_file: Path) -> None: + """Every shipped language must carry exactly the keys defined in strings.json.""" + expected = _flatten(_load(STRINGS_FILE)) + actual = _flatten(_load(translation_file)) + + assert not expected - actual, f"{translation_file.name} is missing keys" + assert not actual - expected, f"{translation_file.name} has stale keys" + + +def _enum_descriptions() -> list[Any]: + """Sensor descriptions that report a fixed set of states to the frontend.""" + seen: dict[str, Any] = {} + for description in (*SENSOR_TYPES_WEATHER_API, *SENSOR_TYPES_WSLINK): + if description.device_class is not SensorDeviceClass.ENUM or not description.options: + continue + seen.setdefault(description.translation_key or description.key, description) + return list(seen.values()) + + +@pytest.mark.parametrize( + ("path", "description"), + [(path, description) for path in (STRINGS_FILE, *TRANSLATION_FILES) for description in _enum_descriptions()], + ids=lambda value: value.name if isinstance(value, Path) else (value.translation_key or value.key), +) +def test_enum_states_are_translated(path: Path, description: Any) -> None: + """Each enum state an entity can emit needs a translation - and no extra ones. + + The battery sensors emit `UnitOfBat.UNKNOWN`, whose *value* is "drained"; a state + block keyed on the member name ("unknown") never matches, so the frontend falls back + to showing the raw state. + """ + translation_key = description.translation_key or description.key + states = _load(path)["entity"]["sensor"][translation_key]["state"] + + assert set(states) == {str(option) for option in description.options}, ( + f"{path.name}: state keys for '{translation_key}' do not match the emitted values" + )