From 2b4e1a5f8c9e56e34f1b5c8f486819d0e31c9738 Mon Sep 17 00:00:00 2001 From: SchiZzA Date: Sun, 21 Jun 2026 12:51:35 +0200 Subject: [PATCH] fix: address review findings across forwarders and helpers - wind_dir_to_text: treat 0/missing direction as None (calm) so a missing wind direction no longer renders as North; azimut lambdas pass None too. - windy: log response status (was logging an un-awaited coroutine method); count "unexpected response" toward the disable threshold even when logging is off. - pocasi: disable threshold >3 -> >=3 to match Windy's 3-strike behavior. - ecowitt: always attach device_info to native sensors, including unknown sensor types (previously left orphaned without a device). - const: stop remapping WSLink connection flags (t1cn/t234cXcn) into the payload; they had no entity and leaked ghost *_connection keys into data and persisted SENSORS_TO_LOAD. Gating uses the raw keys and is unaffected. - config_flow: fix WSLink port fallback typo 433 -> 443; drop mutable default argument on async_step_init. Co-Authored-By: Claude Opus 4.8 (1M context) --- custom_components/sws12500/config_flow.py | 4 ++-- custom_components/sws12500/const.py | 12 ++++------ custom_components/sws12500/ecowitt.py | 23 +++++++++++-------- custom_components/sws12500/pocasti_cz.py | 2 +- custom_components/sws12500/sensors_weather.py | 2 +- custom_components/sws12500/sensors_wslink.py | 2 +- custom_components/sws12500/utils.py | 12 ++++++---- custom_components/sws12500/windy_func.py | 8 +++++-- 8 files changed, 36 insertions(+), 29 deletions(-) diff --git a/custom_components/sws12500/config_flow.py b/custom_components/sws12500/config_flow.py index a67e62b..c4d26fc 100644 --- a/custom_components/sws12500/config_flow.py +++ b/custom_components/sws12500/config_flow.py @@ -141,7 +141,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): self.wslink_addon_port = {WSLINK_ADDON_PORT: self.config_entry.options.get(WSLINK_ADDON_PORT, 443)} - async def async_step_init(self, user_input: dict[str, Any] = {}): + async def async_step_init(self, user_input: dict[str, Any] | None = None): """Manage the options - show menu first.""" _ = user_input return self.async_show_menu( @@ -295,7 +295,7 @@ class ConfigOptionsFlowHandler(OptionsFlow): await self._get_entry_data() if not (port := self.wslink_addon_port.get(WSLINK_ADDON_PORT)): - port = 433 + port = 443 wslink_port_schema = { vol.Required(WSLINK_ADDON_PORT, default=port): int, diff --git a/custom_components/sws12500/const.py b/custom_components/sws12500/const.py index 6b7cfd0..37a484d 100644 --- a/custom_components/sws12500/const.py +++ b/custom_components/sws12500/const.py @@ -262,14 +262,10 @@ REMAP_WSLINK_ITEMS: dict[str, str] = { "t1uvi": UV, "t234c1tem": CH2_TEMP, "t234c1hum": CH2_HUMIDITY, - "t1cn": OUTSIDE_CONNECTION, - "t234c1cn": CH2_CONNECTION, - "t234c2cn": CH3_CONNECTION, - "t234c3cn": CH4_CONNECTION, - "t234c4cn": CH5_CONNECTION, - "t234c5cn": CH6_CONNECTION, - "t234c6cn": CH7_CONNECTION, - "t234c7cn": CH8_CONNECTION, + # NOTE: connection flags (t1cn / t234cXcn / t9cn) are intentionally NOT remapped. + # They are used only as gating inputs (see CONNECTION_GATED_SENSORS), which read the + # raw payload keys. Remapping them used to leak ghost "*_connection" keys (with no + # entity) into the coordinator data and into persisted SENSORS_TO_LOAD. "t1chill": CHILL_INDEX, "t1heat": HEAT_INDEX, "t1rainhr": HOURLY_RAIN, diff --git a/custom_components/sws12500/ecowitt.py b/custom_components/sws12500/ecowitt.py index c174279..cf6bb5b 100644 --- a/custom_components/sws12500/ecowitt.py +++ b/custom_components/sws12500/ecowitt.py @@ -251,7 +251,8 @@ class EcoWittNativeSensor(SensorEntity): self._attr_translation_key = None # we do not have translation_keys for native sensors self._attr_name = sensor.name # default name, can be overridden by translation_key if we had one - # set HomeAssistant metadata from aioecowitt sensor type + # set HomeAssistant metadata from aioecowitt sensor type. + # Unknown types still get a usable entity (raw value, no device class/unit). ha_meta = STYPE_TO_HA.get(sensor.stype) if ha_meta: device_class, unit, state_class = ha_meta @@ -259,15 +260,17 @@ class EcoWittNativeSensor(SensorEntity): self._attr_native_unit_of_measurement = unit self._attr_state_class = state_class - station = sensor.station - self._attr_device_info = DeviceInfo( - connections=set(), - name=f"Ecowitt {station.model}" if station else "Ecowitt station", - entry_type=DeviceEntryType.SERVICE, - identifiers={(DOMAIN, f"ecowitt_{station.key}" if station else "ecowitt")}, - manufacturer="Ecowitt impl. from Schizza for SWS12500", - model=station.model if station else None, - ) + # Always attach device info so the entity is grouped under the Ecowitt + # station device even when its sensor type has no HA mapping. + station = sensor.station + self._attr_device_info = DeviceInfo( + connections=set(), + name=f"Ecowitt {station.model}" if station else "Ecowitt station", + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, f"ecowitt_{station.key}" if station else "ecowitt")}, + manufacturer="Ecowitt impl. from Schizza for SWS12500", + model=station.model if station else None, + ) @property def native_value(self) -> StateType | datetime: # pyright: ignore[reportIncompatibleVariableOverride] diff --git a/custom_components/sws12500/pocasti_cz.py b/custom_components/sws12500/pocasti_cz.py index 4cf96f6..73496e3 100644 --- a/custom_components/sws12500/pocasti_cz.py +++ b/custom_components/sws12500/pocasti_cz.py @@ -156,7 +156,7 @@ class PocasiPush: self.last_error = str(ex) _LOGGER.critical("Invalid response from Pocasi Meteo: %s", str(ex)) self.invalid_response_count += 1 - if self.invalid_response_count > 3: + if self.invalid_response_count >= 3: _LOGGER.critical(POCASI_CZ_UNEXPECTED) self.enabled = False await update_options(self.hass, self.config, POCASI_CZ_ENABLED, False) diff --git a/custom_components/sws12500/sensors_weather.py b/custom_components/sws12500/sensors_weather.py index 2de4e5f..6e8f60e 100644 --- a/custom_components/sws12500/sensors_weather.py +++ b/custom_components/sws12500/sensors_weather.py @@ -132,7 +132,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( key=WIND_AZIMUT, icon="mdi:sign-direction", - value_from_data_fn=lambda dir: wind_dir_to_text(dir.get(WIND_DIR, 0.0)), + value_from_data_fn=lambda dir: wind_dir_to_text(dir.get(WIND_DIR)), device_class=SensorDeviceClass.ENUM, options=[e.value for e in UnitOfDir], translation_key=WIND_AZIMUT, diff --git a/custom_components/sws12500/sensors_wslink.py b/custom_components/sws12500/sensors_wslink.py index d0f9e22..659b24d 100644 --- a/custom_components/sws12500/sensors_wslink.py +++ b/custom_components/sws12500/sensors_wslink.py @@ -160,7 +160,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = ( WeatherSensorEntityDescription( key=WIND_AZIMUT, icon="mdi:sign-direction", - value_from_data_fn=lambda dir: wind_dir_to_text(dir.get(WIND_DIR, 0.0)), + value_from_data_fn=lambda dir: wind_dir_to_text(dir.get(WIND_DIR)), device_class=SensorDeviceClass.ENUM, options=[e.value for e in UnitOfDir], translation_key=WIND_AZIMUT, diff --git a/custom_components/sws12500/utils.py b/custom_components/sws12500/utils.py index a42a829..d1229ac 100644 --- a/custom_components/sws12500/utils.py +++ b/custom_components/sws12500/utils.py @@ -191,15 +191,19 @@ def check_disabled(items: dict[str, str], config_entry: ConfigEntry) -> list[str def wind_dir_to_text(deg: float) -> UnitOfDir | None: """Return wind direction in text representation. + A direction of 0 - or a missing/invalid value - is treated as "no reading" + (calm) and returns None, so a missing wind direction does not render as North. + Returns UnitOfDir or None """ _deg = to_float(deg) - if _deg is not None: - _LOGGER.debug("wind_dir: %s", AZIMUT[int(abs((_deg - 11.25) % 360) / 22.5)]) - return AZIMUT[int(abs((_deg - 11.25) % 360) / 22.5)] + if _deg is None or _deg == 0: + return None - return None + azimut = AZIMUT[int(abs((_deg - 11.25) % 360) / 22.5)] + _LOGGER.debug("wind_dir: %s", azimut) + return azimut def battery_level(battery: int | str | None) -> UnitOfBat: diff --git a/custom_components/sws12500/windy_func.py b/custom_components/sws12500/windy_func.py index 330b503..46b62f1 100644 --- a/custom_components/sws12500/windy_func.py +++ b/custom_components/sws12500/windy_func.py @@ -104,7 +104,9 @@ class WindyPush: """Verify answer form Windy.""" if self.log and response: - _LOGGER.info("Windy raw response: %s", response.text) + # response.text is a coroutine; we are in a sync method here, so log the + # status instead of awaiting/logging a bound method object. + _LOGGER.info("Windy raw response status: %s", response.status) if response.status == 200: raise WindySuccess @@ -270,8 +272,10 @@ class WindyPush: else: self.last_status = "unexpected_response" self.last_error = "Unexpected response from Windy." + # Always count unexpected responses toward the disable threshold, + # regardless of the logging setting. + self.invalid_response_count += 1 if self.log: - self.invalid_response_count += 1 _LOGGER.debug( "Unexpected response from Windy. Max retries before disabling resend function: %s", (WINDY_MAX_RETRIES - self.invalid_response_count),