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>
parent
bf6a02c5b3
commit
1fe4d6da45
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
||||||
|
|
|
||||||
|
|
@ -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()}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue