fix(security)+i18n: stop health-attribute leak, mask secrets in logs, sync translations

Security (from the deep audit):
- S1: health "integration_health" sensor no longer dumps the full snapshot in
  extra_state_attributes (readable by any HA user). public_health_snapshot()
  strips internal network details (source IP, internal URLs, raw add-on status);
  full data stays on the authenticated endpoint and admin-only diagnostics.
- S2: anonymize() now masks the Ecowitt passkey/PASSKEY.
- S3: Windy station id masked before logging the dataset.
- S4: diagnostics redacts internal network fields (source IP, URLs, raw_status).

i18n (Q1/Q2):
- Sync strings.json + en.json with the actual flow: add wslink_port_setup step,
  the ecowitt/pocasi/wslink_port_setup menu entries, the pocasi_* and
  windy_key_required error keys, the stale_sensors_detected issue; fix wrong data
  keys (WSLINK->wslink + legacy_enabled, POCASI_CZ_SEND_INTERVAL->POCASI_SEND_INTERVAL,
  pocasi_enabled_checkbox->pocasi_enabled_chcekbox); translate the English ecowitt
  options step (was Czech); de-duplicate the legacy-battery issue description.
- Q2: rename wbgt_index -> wbgt_temp to match the entity translation_key.
- cs.json: add windy_key_required and fix the pocasi data/data_description keys.

Tests: 300 passing, 100% coverage maintained; basedpyright clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SchiZzA 2026-06-21 18:08:34 +02:00
parent bf6a02c5b3
commit 1fe4d6da45
No known key found for this signature in database
10 changed files with 646 additions and 512 deletions

View File

@ -31,6 +31,13 @@ TO_REDACT = {
"PASSWORD", "PASSWORD",
"wsid", "wsid",
"wspw", "wspw",
# Internal network details from the health snapshot (admin-only download, but
# diagnostics are commonly shared in bug reports).
"home_assistant_source_ip",
"home_assistant_url",
"health_url",
"info_url",
"raw_status",
} }

View File

@ -83,6 +83,28 @@ def _sanitize_path(path: str) -> str:
return path return path
# Fields under "addon" that reveal internal network topology. They must not be
# exposed via entity attributes (readable by any HA user, incl. non-admins).
_SENSITIVE_ADDON_FIELDS: frozenset[str] = frozenset(
{"health_url", "info_url", "home_assistant_url", "home_assistant_source_ip", "raw_status"}
)
def public_health_snapshot(data: dict[str, Any]) -> dict[str, Any]:
"""Return a copy of the health snapshot safe to expose to any HA user.
Removes internal network details (HA source IP, internal URLs, raw add-on
status). The full snapshot stays available only via the authenticated
`/station/health` endpoint and the admin-only (redacted) diagnostics download.
"""
public: dict[str, Any] = deepcopy(data)
addon = checked(public.get("addon"), dict[str, Any])
if addon is not None:
for field in _SENSITIVE_ADDON_FIELDS:
addon.pop(field, None)
return public
def _empty_forwarding_state(enabled: bool) -> dict[str, Any]: def _empty_forwarding_state(enabled: bool) -> dict[str, Any]:
"""Build the default forwarding status payload.""" """Build the default forwarding status payload."""
return { return {

View File

@ -20,7 +20,7 @@ from homeassistant.util import dt as dt_util
from .const import DOMAIN from .const import DOMAIN
from .data import SWSConfigEntry from .data import SWSConfigEntry
from .health_coordinator import HealthCoordinator from .health_coordinator import HealthCoordinator, public_health_snapshot
if TYPE_CHECKING: if TYPE_CHECKING:
from .health_coordinator import HealthCoordinator from .health_coordinator import HealthCoordinator
@ -248,11 +248,20 @@ class HealthDiagnosticSensor( # pyright: ignore[reportIncompatibleVariableOverr
@property @property
def extra_state_attributes(self) -> dict[str, Any] | None: # pyright: ignore[reportIncompatibleVariableOverride] def extra_state_attributes(self) -> dict[str, Any] | None: # pyright: ignore[reportIncompatibleVariableOverride]
"""Expose the full health JSON on the main health sensor for debugging.""" """Expose the health JSON on the main health sensor for debugging.
Entity attributes are readable by any HA user, so internal network details
(source IP, internal URLs, raw add-on status) are stripped here; they remain
available via the authenticated endpoint and the admin-only diagnostics.
"""
if self.entity_description.key != "integration_health": if self.entity_description.key != "integration_health":
return None return None
return checked_or(self.coordinator.data, dict[str, Any], None) data = checked_or(self.coordinator.data, dict[str, Any], None)
if data is None:
return None
return public_health_snapshot(data)
@cached_property @cached_property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:

View File

@ -50,7 +50,11 @@
"valid_credentials_key": "Provide valid API KEY.", "valid_credentials_key": "Provide valid API KEY.",
"valid_credentials_match": "API ID and API KEY should not be the same.", "valid_credentials_match": "API ID and API KEY should not be the same.",
"windy_id_required": "Windy API ID is required if you want to enable this function.", "windy_id_required": "Windy API ID is required if you want to enable this function.",
"windy_pw_required": "Windy API password is required if you want to enable this function." "windy_pw_required": "Windy API password is required if you want to enable this function.",
"windy_key_required": "Windy station ID and password are required if you want to enable this function.",
"pocasi_id_required": "Pocasi Meteo API ID is required if you want to enable this function.",
"pocasi_key_required": "Pocasi Meteo API key is required if you want to enable this function.",
"pocasi_send_minimum": "The resend interval must be at least 12 seconds."
}, },
"step": { "step": {
"init": { "init": {
@ -58,7 +62,10 @@
"description": "Choose what do you want to configure. If basic access or resending data for Windy site", "description": "Choose what do you want to configure. If basic access or resending data for Windy site",
"menu_options": { "menu_options": {
"basic": "Basic - configure credentials for Weather Station", "basic": "Basic - configure credentials for Weather Station",
"windy": "Windy configuration" "wslink_port_setup": "WSLink add-on port",
"ecowitt": "Ecowitt configuration",
"windy": "Windy configuration",
"pocasi": "Pocasi Meteo CZ configuration"
} }
}, },
"basic": { "basic": {
@ -67,14 +74,16 @@
"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 API",
"legacy_enabled": "Enable PWS/WSLink endpoint",
"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.",
"legacy_enabled": "Enable the PWS/WSLink endpoint. Turn off for an Ecowitt-only setup."
} }
}, },
"windy": { "windy": {
@ -98,28 +107,38 @@
"data": { "data": {
"POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP", "POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP",
"POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP", "POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP",
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds", "POCASI_SEND_INTERVAL": "Resend interval in seconds",
"pocasi_enabled_checkbox": "Enable resending data to Pocasi Meteo", "pocasi_enabled_chcekbox": "Enable resending data to Pocasi Meteo",
"pocasi_logger_checkbox": "Log data and responses" "pocasi_logger_checkbox": "Log data and responses"
}, },
"data_description": { "data_description": {
"POCASI_CZ_API_ID": "You can obtain your ID in Pocasi Meteo App", "POCASI_CZ_API_ID": "You can obtain your ID in Pocasi Meteo App",
"POCASI_CZ_API_KEY": "You can obtain your KEY in Pocasi Meteo App", "POCASI_CZ_API_KEY": "You can obtain your KEY in Pocasi Meteo App",
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)", "POCASI_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)",
"pocasi_enabled_checkbox": "Enables resending data to Pocasi Meteo", "pocasi_enabled_chcekbox": "Enables resending data to Pocasi Meteo",
"pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer" "pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer"
} }
}, },
"ecowitt": { "ecowitt": {
"description": "Nastavení pro Ecowitt", "description": "Ecowitt configuration.",
"title": "Konfigurace pro stanice Ecowitt", "title": "Ecowitt station configuration",
"data": { "data": {
"ecowitt_webhook_id": "Unikátní webhook ID", "ecowitt_webhook_id": "Unique webhook ID",
"ecowitt_enabled": "Povolit data ze stanice Ecowitt" "ecowitt_enabled": "Enable Ecowitt station data"
}, },
"data_description": { "data_description": {
"ecowitt_webhook_id": "Nastavení pro stanici: {url}:{port}/weatherhub/{webhook_id}", "ecowitt_webhook_id": "Set your Ecowitt station to send data to the endpoint: {url}:{port}/weatherhub/{webhook_id}",
"ecowitt_enabled": "Povolit přijímání dat ze stanic Ecowitt" "ecowitt_enabled": "Enable receiving data from Ecowitt stations"
}
},
"wslink_port_setup": {
"title": "WSLink add-on port",
"description": "Set the external port of the WSLink proxy add-on so the integration can reach its health endpoint.",
"data": {
"WSLINK_ADDON_PORT": "WSLink add-on port"
},
"data_description": {
"WSLINK_ADDON_PORT": "The external TCP port the WSLink proxy add-on listens on (default 443)."
} }
}, },
"migration": { "migration": {
@ -361,7 +380,7 @@
"yearly_rain": { "yearly_rain": {
"name": "Yearly precipitation" "name": "Yearly precipitation"
}, },
"wbgt_index": { "wbgt_temp": {
"name": "WBGT index" "name": "WBGT index"
}, },
"hcho": { "hcho": {
@ -478,7 +497,11 @@
"issues": { "issues": {
"legacy_battery_sensor_deprecated": { "legacy_battery_sensor_deprecated": {
"title": "Legacy battery sensor detected", "title": "Legacy battery sensor detected",
"description": "Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings -> Devices & Services -> SWS 12500." "description": "Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings -> Devices & Services -> SWS 12500."
},
"stale_sensors_detected": {
"title": "Stale sensors detected",
"description": "These sensors have not reported data for an extended period and may be misconfigured or out of range: {sensors}. If you no longer use them, remove them from the integration options."
} }
}, },
"notify": { "notify": {

View File

@ -51,6 +51,7 @@
"valid_credentials_match": "API ID a API KEY nesmějí být stejné!", "valid_credentials_match": "API ID a API KEY nesmějí být stejné!",
"windy_id_required": "Je vyžadováno Windy ID, pokud chcete aktivovat přeposílání dat na Windy", "windy_id_required": "Je vyžadováno Windy ID, pokud chcete aktivovat přeposílání dat na Windy",
"windy_pw_required": "Je vyžadován Windy KEY, pokud chcete aktivovat přeposílání dat na Windy", "windy_pw_required": "Je vyžadován Windy KEY, pokud chcete aktivovat přeposílání dat na Windy",
"windy_key_required": "Pro aktivaci je vyžadováno Windy ID i heslo stanice.",
"pocasi_id_required": "Je vyžadován Počasí ID, pokud chcete aktivovat přeposílání dat na Počasí Meteo CZ", "pocasi_id_required": "Je vyžadován Počasí ID, pokud chcete aktivovat přeposílání dat na Počasí Meteo CZ",
"pocasi_key_required": "Klíč k účtu Počasí Meteo je povinný.", "pocasi_key_required": "Klíč k účtu Počasí Meteo je povinný.",
"pocasi_send_minimum": "Minimální interval pro přeposílání je 12 sekund." "pocasi_send_minimum": "Minimální interval pro přeposílání je 12 sekund."
@ -107,15 +108,15 @@
"data": { "data": {
"POCASI_CZ_API_ID": "ID účtu na Počasí Meteo", "POCASI_CZ_API_ID": "ID účtu na Počasí Meteo",
"POCASI_CZ_API_KEY": "Klíč (Key) k účtu Počasí Meteo", "POCASI_CZ_API_KEY": "Klíč (Key) k účtu Počasí Meteo",
"POCASI_CZ_SEND_INTERVAL": "Interval v sekundách", "POCASI_SEND_INTERVAL": "Interval v sekundách",
"pocasi_enabled_chcekbox": "Povolit přeposílání dat na server Počasí Meteo", "pocasi_enabled_chcekbox": "Povolit přeposílání dat na server Počasí Meteo",
"pocasi_logger_checkbox": "Logovat data a odpovědi z Počasí Meteo" "pocasi_logger_checkbox": "Logovat data a odpovědi z Počasí Meteo"
}, },
"data_description": { "data_description": {
"POCASI_API_ID": "ID získáte ve své aplikaci Počasí Meteo", "POCASI_CZ_API_ID": "ID získáte ve své aplikaci Počasí Meteo",
"POCASI_API_KEY": "Klíč (Key) získáte ve své aplikaci Počasí Meteo", "POCASI_CZ_API_KEY": "Klíč (Key) získáte ve své aplikaci Počasí Meteo",
"POCASI_SEND_INTERVAL": "Interval v jakém se mají data na server přeposílat (minimum 12s, defaultně 30s)", "POCASI_SEND_INTERVAL": "Interval v jakém se mají data na server přeposílat (minimum 12s, defaultně 30s)",
"pocasi_enabled_checkbox": "Zapne přeposílání data na server Počasí Meteo", "pocasi_enabled_chcekbox": "Zapne přeposílání data na server Počasí Meteo",
"pocasi_logger_checkbox": "Zapnout pouze v případě, že chcete zaslat ladící informace vývojáři." "pocasi_logger_checkbox": "Zapnout pouze v případě, že chcete zaslat ladící informace vývojáři."
} }
}, },

View File

@ -50,7 +50,11 @@
"valid_credentials_key": "Provide valid API KEY.", "valid_credentials_key": "Provide valid API KEY.",
"valid_credentials_match": "API ID and API KEY should not be the same.", "valid_credentials_match": "API ID and API KEY should not be the same.",
"windy_id_required": "Windy API ID is required if you want to enable this function.", "windy_id_required": "Windy API ID is required if you want to enable this function.",
"windy_pw_required": "Windy API password is required if you want to enable this function." "windy_pw_required": "Windy API password is required if you want to enable this function.",
"windy_key_required": "Windy station ID and password are required if you want to enable this function.",
"pocasi_id_required": "Pocasi Meteo API ID is required if you want to enable this function.",
"pocasi_key_required": "Pocasi Meteo API key is required if you want to enable this function.",
"pocasi_send_minimum": "The resend interval must be at least 12 seconds."
}, },
"step": { "step": {
"init": { "init": {
@ -58,7 +62,10 @@
"description": "Choose what do you want to configure. If basic access or resending data for Windy site", "description": "Choose what do you want to configure. If basic access or resending data for Windy site",
"menu_options": { "menu_options": {
"basic": "Basic - configure credentials for Weather Station", "basic": "Basic - configure credentials for Weather Station",
"windy": "Windy configuration" "wslink_port_setup": "WSLink add-on port",
"ecowitt": "Ecowitt configuration",
"windy": "Windy configuration",
"pocasi": "Pocasi Meteo CZ configuration"
} }
}, },
"basic": { "basic": {
@ -67,14 +74,16 @@
"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 API",
"legacy_enabled": "Enable PWS/WSLink endpoint",
"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.",
"legacy_enabled": "Enable the PWS/WSLink endpoint. Turn off for an Ecowitt-only setup."
} }
}, },
"windy": { "windy": {
@ -98,28 +107,38 @@
"data": { "data": {
"POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP", "POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP",
"POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP", "POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP",
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds", "POCASI_SEND_INTERVAL": "Resend interval in seconds",
"pocasi_enabled_checkbox": "Enable resending data to Pocasi Meteo", "pocasi_enabled_chcekbox": "Enable resending data to Pocasi Meteo",
"pocasi_logger_checkbox": "Log data and responses" "pocasi_logger_checkbox": "Log data and responses"
}, },
"data_description": { "data_description": {
"POCASI_CZ_API_ID": "You can obtain your ID in Pocasi Meteo App", "POCASI_CZ_API_ID": "You can obtain your ID in Pocasi Meteo App",
"POCASI_CZ_API_KEY": "You can obtain your KEY in Pocasi Meteo App", "POCASI_CZ_API_KEY": "You can obtain your KEY in Pocasi Meteo App",
"POCASI_CZ_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)", "POCASI_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)",
"pocasi_enabled_checkbox": "Enables resending data to Pocasi Meteo", "pocasi_enabled_chcekbox": "Enables resending data to Pocasi Meteo",
"pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer" "pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer"
} }
}, },
"ecowitt": { "ecowitt": {
"description": "Nastavení pro Ecowitt", "description": "Ecowitt configuration.",
"title": "Konfigurace pro stanice Ecowitt", "title": "Ecowitt station configuration",
"data": { "data": {
"ecowitt_webhook_id": "Unikátní webhook ID", "ecowitt_webhook_id": "Unique webhook ID",
"ecowitt_enabled": "Povolit data ze stanice Ecowitt" "ecowitt_enabled": "Enable Ecowitt station data"
}, },
"data_description": { "data_description": {
"ecowitt_webhook_id": "Nastavení pro stanici: {url}:{port}/weatherhub/{webhook_id}", "ecowitt_webhook_id": "Set your Ecowitt station to send data to the endpoint: {url}:{port}/weatherhub/{webhook_id}",
"ecowitt_enabled": "Povolit přijímání dat ze stanic Ecowitt" "ecowitt_enabled": "Enable receiving data from Ecowitt stations"
}
},
"wslink_port_setup": {
"title": "WSLink add-on port",
"description": "Set the external port of the WSLink proxy add-on so the integration can reach its health endpoint.",
"data": {
"WSLINK_ADDON_PORT": "WSLink add-on port"
},
"data_description": {
"WSLINK_ADDON_PORT": "The external TCP port the WSLink proxy add-on listens on (default 443)."
} }
}, },
"migration": { "migration": {
@ -361,7 +380,7 @@
"yearly_rain": { "yearly_rain": {
"name": "Yearly precipitation" "name": "Yearly precipitation"
}, },
"wbgt_index": { "wbgt_temp": {
"name": "WBGT index" "name": "WBGT index"
}, },
"hcho": { "hcho": {
@ -478,11 +497,11 @@
"issues": { "issues": {
"legacy_battery_sensor_deprecated": { "legacy_battery_sensor_deprecated": {
"title": "Legacy battery sensor detected", "title": "Legacy battery sensor detected",
"description": "Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings -> Devices & Services -> SWS 12500." "description": "Old battery sensor entities ({entities}) were found in the entity registry. They have been replaced by binary battery sensors and will be removed in version {remove_version}. Please delete the legacy entities manually via Settings -> Devices & Services -> SWS 12500."
}, },
"stale_sensors_detected": { "stale_sensors_detected": {
"title": "Stale sensor detected", "title": "Stale sensors detected",
"description": "These sensors are configured but haven't reported data recently: {sensors}. They will appear as Unavailable in HA. If they are no longer connected, you can remove them via Settings -> Devices & Services -> SWS 12500." "description": "These sensors have not reported data for an extended period and may be misconfigured or out of range: {sensors}. If you no longer use them, remove them from the integration options."
} }
}, },
"notify": { "notify": {

View File

@ -112,7 +112,7 @@ def anonymize(
- Keep all keys, but mask sensitive values. - Keep all keys, but mask sensitive values.
- Do not raise on unexpected/missing keys. - Do not raise on unexpected/missing keys.
""" """
secrets = {"ID", "PASSWORD", "wsid", "wspw"} secrets = {"ID", "PASSWORD", "wsid", "wspw", "passkey", "PASSKEY"}
return {k: ("***" if k in secrets else v) for k, v in data.items()} return {k: ("***" if k in secrets else v) for k, v in data.items()}

View File

@ -219,7 +219,8 @@ class WindyPush:
headers = {"Authorization": f"Bearer {windy_station_pw}"} headers = {"Authorization": f"Bearer {windy_station_pw}"}
if self.log: if self.log:
_LOGGER.info("Dataset for windy: %s", purged_data) # Mask the station id (a credential) before logging the dataset.
_LOGGER.info("Dataset for windy: %s", {**purged_data, "id": "***"})
session = async_get_clientsession(self.hass) session = async_get_clientsession(self.hass)
try: try:
async with session.get(request_url, params=purged_data, headers=headers) as resp: async with session.get(request_url, params=purged_data, headers=headers) as resp:

View File

@ -663,11 +663,39 @@ def test_sensor_native_value_with_value_fn() -> None:
assert sensor.native_value == "online" assert sensor.native_value == "online"
def test_sensor_extra_state_attributes_for_integration_health() -> None: def test_sensor_extra_state_attributes_strips_internal_fields() -> None:
data = {"integration_status": "online_wu", "addon": {"online": False}} data = {
"integration_status": "online_wu",
"addon": {
"online": True,
"name": "wslink_proxy",
"home_assistant_source_ip": "1.2.3.4",
"health_url": "https://1.2.3.4:443/healthz",
"info_url": "https://1.2.3.4:443/status/internal",
"home_assistant_url": "http://ha:8123",
"raw_status": {"secret": "x"},
},
}
coordinator = _stub_coordinator(data) coordinator = _stub_coordinator(data)
sensor = hs.HealthDiagnosticSensor(coordinator, _description("integration_health")) sensor = hs.HealthDiagnosticSensor(coordinator, _description("integration_health"))
assert sensor.extra_state_attributes == data
attrs = sensor.extra_state_attributes
assert attrs is not None
# Non-sensitive fields pass through.
assert attrs["integration_status"] == "online_wu"
assert attrs["addon"]["online"] is True
assert attrs["addon"]["name"] == "wslink_proxy"
# Internal network details are stripped from the (any-user-readable) attributes.
for field in ("home_assistant_source_ip", "health_url", "info_url", "home_assistant_url", "raw_status"):
assert field not in attrs["addon"]
# The source snapshot is not mutated (deepcopy).
assert "raw_status" in data["addon"]
def test_sensor_extra_state_attributes_none_data() -> None:
coordinator = _stub_coordinator(None)
sensor = hs.HealthDiagnosticSensor(coordinator, _description("integration_health"))
assert sensor.extra_state_attributes is None
def test_sensor_extra_state_attributes_for_other_keys() -> None: def test_sensor_extra_state_attributes_for_other_keys() -> None:
@ -676,6 +704,13 @@ def test_sensor_extra_state_attributes_for_other_keys() -> None:
assert sensor.extra_state_attributes is None assert sensor.extra_state_attributes is None
def test_public_health_snapshot_handles_missing_or_nondict_addon() -> None:
# addon missing -> returned as-is (copy).
assert hc.public_health_snapshot({"integration_status": "x"}) == {"integration_status": "x"}
# addon not a dict -> left untouched.
assert hc.public_health_snapshot({"addon": "nope"}) == {"addon": "nope"}
def test_sensor_device_info() -> None: def test_sensor_device_info() -> None:
coordinator = _stub_coordinator({}) coordinator = _stub_coordinator({})
sensor = hs.HealthDiagnosticSensor(coordinator, _description("active_protocol")) sensor = hs.HealthDiagnosticSensor(coordinator, _description("active_protocol"))

View File

@ -1,10 +1,27 @@
"""Coverage for to_int / to_float edge cases.""" """Coverage for to_int / to_float edge cases and anonymize() masking."""
from __future__ import annotations from __future__ import annotations
import pytest import pytest
from custom_components.sws12500.utils import to_float, to_int from custom_components.sws12500.utils import anonymize, to_float, to_int
def test_anonymize_masks_all_known_secrets() -> None:
raw = {
"ID": "id",
"PASSWORD": "pw",
"wsid": "ws",
"wspw": "wp",
"passkey": "ecowitt-secret",
"PASSKEY": "ecowitt-secret",
"tempf": "68",
}
out = anonymize(raw)
for key in ("ID", "PASSWORD", "wsid", "wspw", "passkey", "PASSKEY"):
assert out[key] == "***"
# Non-secret values pass through unchanged.
assert out["tempf"] == "68"
@pytest.mark.parametrize( @pytest.mark.parametrize(