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)}
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(
@ -296,7 +296,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,

View File

@ -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,

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_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]

View File

@ -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)

View File

@ -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,

View File

@ -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,

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:
"""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:

View File

@ -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),