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) <noreply@anthropic.com>
SchiZzA 2026-06-21 12:51:35 +02:00
parent b72efa5985
commit 25341f79b3
No known key found for this signature in database
8 changed files with 36 additions and 29 deletions

View File

@ -142,7 +142,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
self.wslink_addon_port = {WSLINK_ADDON_PORT: self.config_entry.options.get(WSLINK_ADDON_PORT, 443)} 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.""" """Manage the options - show menu first."""
_ = user_input _ = user_input
return self.async_show_menu( return self.async_show_menu(
@ -296,7 +296,7 @@ class ConfigOptionsFlowHandler(OptionsFlow):
await self._get_entry_data() await self._get_entry_data()
if not (port := self.wslink_addon_port.get(WSLINK_ADDON_PORT)): if not (port := self.wslink_addon_port.get(WSLINK_ADDON_PORT)):
port = 433 port = 443
wslink_port_schema = { wslink_port_schema = {
vol.Required(WSLINK_ADDON_PORT, default=port): int, vol.Required(WSLINK_ADDON_PORT, default=port): int,

View File

@ -262,14 +262,10 @@ REMAP_WSLINK_ITEMS: dict[str, str] = {
"t1uvi": UV, "t1uvi": UV,
"t234c1tem": CH2_TEMP, "t234c1tem": CH2_TEMP,
"t234c1hum": CH2_HUMIDITY, "t234c1hum": CH2_HUMIDITY,
"t1cn": OUTSIDE_CONNECTION, # NOTE: connection flags (t1cn / t234cXcn / t9cn) are intentionally NOT remapped.
"t234c1cn": CH2_CONNECTION, # They are used only as gating inputs (see CONNECTION_GATED_SENSORS), which read the
"t234c2cn": CH3_CONNECTION, # raw payload keys. Remapping them used to leak ghost "*_connection" keys (with no
"t234c3cn": CH4_CONNECTION, # entity) into the coordinator data and into persisted SENSORS_TO_LOAD.
"t234c4cn": CH5_CONNECTION,
"t234c5cn": CH6_CONNECTION,
"t234c6cn": CH7_CONNECTION,
"t234c7cn": CH8_CONNECTION,
"t1chill": CHILL_INDEX, "t1chill": CHILL_INDEX,
"t1heat": HEAT_INDEX, "t1heat": HEAT_INDEX,
"t1rainhr": HOURLY_RAIN, "t1rainhr": HOURLY_RAIN,

View File

@ -251,7 +251,8 @@ class EcoWittNativeSensor(SensorEntity):
self._attr_translation_key = None # we do not have translation_keys for native sensors 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 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) ha_meta = STYPE_TO_HA.get(sensor.stype)
if ha_meta: if ha_meta:
device_class, unit, state_class = 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_native_unit_of_measurement = unit
self._attr_state_class = state_class self._attr_state_class = state_class
station = sensor.station # Always attach device info so the entity is grouped under the Ecowitt
self._attr_device_info = DeviceInfo( # station device even when its sensor type has no HA mapping.
connections=set(), station = sensor.station
name=f"Ecowitt {station.model}" if station else "Ecowitt station", self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE, connections=set(),
identifiers={(DOMAIN, f"ecowitt_{station.key}" if station else "ecowitt")}, name=f"Ecowitt {station.model}" if station else "Ecowitt station",
manufacturer="Ecowitt impl. from Schizza for SWS12500", entry_type=DeviceEntryType.SERVICE,
model=station.model if station else None, 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 @property
def native_value(self) -> StateType | datetime: # pyright: ignore[reportIncompatibleVariableOverride] def native_value(self) -> StateType | datetime: # pyright: ignore[reportIncompatibleVariableOverride]

View File

@ -156,7 +156,7 @@ class PocasiPush:
self.last_error = str(ex) self.last_error = str(ex)
_LOGGER.critical("Invalid response from Pocasi Meteo: %s", str(ex)) _LOGGER.critical("Invalid response from Pocasi Meteo: %s", str(ex))
self.invalid_response_count += 1 self.invalid_response_count += 1
if self.invalid_response_count > 3: if self.invalid_response_count >= 3:
_LOGGER.critical(POCASI_CZ_UNEXPECTED) _LOGGER.critical(POCASI_CZ_UNEXPECTED)
self.enabled = False self.enabled = False
await update_options(self.hass, self.config, POCASI_CZ_ENABLED, False) await update_options(self.hass, self.config, POCASI_CZ_ENABLED, False)

View File

@ -132,7 +132,7 @@ SENSOR_TYPES_WEATHER_API: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_AZIMUT, key=WIND_AZIMUT,
icon="mdi:sign-direction", 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, device_class=SensorDeviceClass.ENUM,
options=[e.value for e in UnitOfDir], options=[e.value for e in UnitOfDir],
translation_key=WIND_AZIMUT, translation_key=WIND_AZIMUT,

View File

@ -160,7 +160,7 @@ SENSOR_TYPES_WSLINK: tuple[WeatherSensorEntityDescription, ...] = (
WeatherSensorEntityDescription( WeatherSensorEntityDescription(
key=WIND_AZIMUT, key=WIND_AZIMUT,
icon="mdi:sign-direction", 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, device_class=SensorDeviceClass.ENUM,
options=[e.value for e in UnitOfDir], options=[e.value for e in UnitOfDir],
translation_key=WIND_AZIMUT, translation_key=WIND_AZIMUT,

View File

@ -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: def wind_dir_to_text(deg: float) -> UnitOfDir | None:
"""Return wind direction in text representation. """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 Returns UnitOfDir or None
""" """
_deg = to_float(deg) _deg = to_float(deg)
if _deg is not None: if _deg is None or _deg == 0:
_LOGGER.debug("wind_dir: %s", AZIMUT[int(abs((_deg - 11.25) % 360) / 22.5)]) return None
return AZIMUT[int(abs((_deg - 11.25) % 360) / 22.5)]
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: def battery_level(battery: int | str | None) -> UnitOfBat:

View File

@ -104,7 +104,9 @@ class WindyPush:
"""Verify answer form Windy.""" """Verify answer form Windy."""
if self.log and response: 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: if response.status == 200:
raise WindySuccess raise WindySuccess
@ -270,8 +272,10 @@ class WindyPush:
else: else:
self.last_status = "unexpected_response" self.last_status = "unexpected_response"
self.last_error = "Unexpected response from Windy." 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: if self.log:
self.invalid_response_count += 1
_LOGGER.debug( _LOGGER.debug(
"Unexpected response from Windy. Max retries before disabling resend function: %s", "Unexpected response from Windy. Max retries before disabling resend function: %s",
(WINDY_MAX_RETRIES - self.invalid_response_count), (WINDY_MAX_RETRIES - self.invalid_response_count),