no-mistakes(review): Migrate Pocasi option key, fix typos and Windy status
parent
5e4b02fd6e
commit
1b69737408
|
|
@ -46,6 +46,7 @@ from .const import (
|
|||
ECOWITT_URL_PREFIX,
|
||||
HEALTH_URL,
|
||||
POCASI_CZ_ENABLED,
|
||||
POCASI_CZ_ENABLED_LEGACY,
|
||||
SENSORS_TO_LOAD,
|
||||
WINDY_ENABLED,
|
||||
WSLINK,
|
||||
|
|
@ -61,6 +62,34 @@ from .staleness import update_stale_sensors_issue
|
|||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.BINARY_SENSOR]
|
||||
|
||||
# Keep in sync with `ConfigFlowHandler.VERSION`.
|
||||
CONFIG_ENTRY_VERSION: int = 2
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: SWSConfigEntry) -> bool:
|
||||
"""Migrate an old config entry.
|
||||
|
||||
Version 2 renames the Pocasi Meteo enable flag from the misspelled
|
||||
`pocasi_enabled_chcekbox` to `pocasi_enabled_checkbox`. Without moving the persisted
|
||||
value, forwarding would silently switch off for every existing install.
|
||||
"""
|
||||
|
||||
if entry.version > CONFIG_ENTRY_VERSION:
|
||||
# Downgrades are not supported - the entry was written by a newer version.
|
||||
return False
|
||||
|
||||
if entry.version < 2:
|
||||
options = dict(entry.options)
|
||||
if POCASI_CZ_ENABLED_LEGACY in options:
|
||||
legacy_value = options.pop(POCASI_CZ_ENABLED_LEGACY)
|
||||
# Never clobber an already-correct key (both may exist after a partial upgrade).
|
||||
options.setdefault(POCASI_CZ_ENABLED, legacy_value)
|
||||
_LOGGER.debug("Migrated Pocasi Meteo enable flag to %s.", POCASI_CZ_ENABLED)
|
||||
|
||||
hass.config_entries.async_update_entry(entry, options=options, version=2)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register_path(
|
||||
hass: HomeAssistant,
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||
vol.Optional(DEV_DBG): bool,
|
||||
}
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
|
||||
async def async_step_user(self, user_input: Any = None):
|
||||
"""Handle the initial step."""
|
||||
|
|
|
|||
|
|
@ -224,10 +224,14 @@ POCASI_CZ_API_KEY = "POCASI_CZ_API_KEY"
|
|||
POCASI_CZ_API_ID = "POCASI_CZ_API_ID"
|
||||
POCASI_CZ_SEND_INTERVAL = "POCASI_SEND_INTERVAL"
|
||||
POCASI_CZ_ENABLED = "pocasi_enabled_checkbox"
|
||||
# Misspelled key used by config entries created before version 2 (see `async_migrate_entry`).
|
||||
POCASI_CZ_ENABLED_LEGACY: Final = "pocasi_enabled_chcekbox"
|
||||
POCASI_CZ_LOGGER_ENABLED = "pocasi_logger_checkbox"
|
||||
POCASI_INVALID_KEY: Final = "Pocasi Meteo refused to accept data. Invalid ID/Key combination?"
|
||||
POCASI_CZ_SUCCESS: Final = "Successfully sent data to Pocasi Meteo"
|
||||
POCASI_CZ_UNEXPECTED: Final = "Pocasi Meteo responded unexpectedly 3 times in row. Resending is now disabled!"
|
||||
POCASI_CZ_UNEXPECTED: Final = (
|
||||
f"Pocasi Meteo responded unexpectedly {POCASI_CZ_MAX_RETRIES} times in row. Resending is now disabled!"
|
||||
)
|
||||
|
||||
WINDY_STATION_ID = "WINDY_STATION_ID"
|
||||
WINDY_STATION_PW = "WINDY_STATION_PWD"
|
||||
|
|
@ -238,7 +242,9 @@ WINDY_INVALID_KEY: Final = (
|
|||
"Windy API KEY is invalid. Send data to Windy is now disabled. Check your API KEY and try again."
|
||||
)
|
||||
WINDY_SUCCESS: Final = "Windy successfully sent data and data was successfully inserted by Windy API"
|
||||
WINDY_UNEXPECTED: Final = "Windy responded unexpectedly 3 times in a row. Send to Windy is now disabled!"
|
||||
WINDY_UNEXPECTED: Final = (
|
||||
f"Windy responded unexpectedly {WINDY_MAX_RETRIES} times in a row. Send to Windy is now disabled!"
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ class PocasiPush:
|
|||
self.last_error = f"Unexpected HTTP status {http_status} from Pocasi Meteo."
|
||||
self.invalid_response_count += 1
|
||||
_LOGGER.warning(
|
||||
"Unexpected HTTP status %s from Pocasi Meteo. Rentries before disabling resend: %s",
|
||||
"Unexpected HTTP status %s from Pocasi Meteo. Retries before disabling resend: %s",
|
||||
http_status,
|
||||
POCASI_CZ_MAX_RETRIES - self.invalid_response_count,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -110,14 +110,14 @@
|
|||
"POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP",
|
||||
"POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP",
|
||||
"POCASI_SEND_INTERVAL": "Resend interval in seconds",
|
||||
"pocasi_enabled_chcekbox": "Enable resending data to Pocasi Meteo",
|
||||
"pocasi_enabled_checkbox": "Enable resending data to Pocasi Meteo",
|
||||
"pocasi_logger_checkbox": "Log data and responses"
|
||||
},
|
||||
"data_description": {
|
||||
"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_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)",
|
||||
"pocasi_enabled_chcekbox": "Enables resending data to Pocasi Meteo",
|
||||
"pocasi_enabled_checkbox": "Enables resending data to Pocasi Meteo",
|
||||
"pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -110,14 +110,14 @@
|
|||
"POCASI_CZ_API_ID": "ID účtu na Počasí Meteo",
|
||||
"POCASI_CZ_API_KEY": "Klíč (Key) k účtu Počasí Meteo",
|
||||
"POCASI_SEND_INTERVAL": "Interval v sekundách",
|
||||
"pocasi_enabled_chcekbox": "Povolit přeposílání dat na server Počasí Meteo",
|
||||
"pocasi_enabled_checkbox": "Povolit přeposílání dat na server Počasí Meteo",
|
||||
"pocasi_logger_checkbox": "Logovat data a odpovědi z Počasí Meteo"
|
||||
},
|
||||
"data_description": {
|
||||
"POCASI_CZ_API_ID": "ID 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_enabled_chcekbox": "Zapne přeposílání data na server Počasí Meteo",
|
||||
"pocasi_enabled_checkbox": "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."
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -110,14 +110,14 @@
|
|||
"POCASI_CZ_API_ID": "ID from your Pocasi Meteo APP",
|
||||
"POCASI_CZ_API_KEY": "Key from your Pocasi Meteo APP",
|
||||
"POCASI_SEND_INTERVAL": "Resend interval in seconds",
|
||||
"pocasi_enabled_chcekbox": "Enable resending data to Pocasi Meteo",
|
||||
"pocasi_enabled_checkbox": "Enable resending data to Pocasi Meteo",
|
||||
"pocasi_logger_checkbox": "Log data and responses"
|
||||
},
|
||||
"data_description": {
|
||||
"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_SEND_INTERVAL": "Resend interval in seconds (minimum 12s, default 30s)",
|
||||
"pocasi_enabled_chcekbox": "Enables resending data to Pocasi Meteo",
|
||||
"pocasi_enabled_checkbox": "Enables resending data to Pocasi Meteo",
|
||||
"pocasi_logger_checkbox": "Enable only if you want to send debbug data to the developer"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -163,8 +163,10 @@ class WindyPush:
|
|||
|
||||
`enabled` reads the option back, so persisting it here is what actually turns
|
||||
forwarding off.
|
||||
|
||||
`last_status` is deliberately left to the caller so the diagnostics sensor keeps
|
||||
the specific reason (`config_error`, `auth_error`, ...) instead of a generic one.
|
||||
"""
|
||||
self.last_status = "disabled"
|
||||
self.last_error = reason
|
||||
|
||||
if not await update_options(self.hass, self.config, WINDY_ENABLED, False):
|
||||
|
|
@ -251,7 +253,7 @@ class WindyPush:
|
|||
|
||||
# log despite of settings
|
||||
_LOGGER.error(
|
||||
"%s Max rentries before disable resend function: %s",
|
||||
"%s Max retries before disable resend function: %s",
|
||||
WINDY_NOT_INSERTED,
|
||||
(WINDY_MAX_RETRIES - self.invalid_response_count),
|
||||
)
|
||||
|
|
@ -268,7 +270,7 @@ class WindyPush:
|
|||
self.last_status = "duplicate"
|
||||
self.last_error = "Duplicate payload detected by Windy server."
|
||||
_LOGGER.critical(
|
||||
"Duplicate payload detected by Windy server. Will try again later. Max rentries before disabling resend function: %s",
|
||||
"Duplicate payload detected by Windy server. Will try again later. Max retries before disabling resend function: %s",
|
||||
(WINDY_MAX_RETRIES - self.invalid_response_count),
|
||||
)
|
||||
self.invalid_response_count += 1
|
||||
|
|
@ -296,7 +298,7 @@ class WindyPush:
|
|||
self.invalid_response_count += 1
|
||||
if self.log:
|
||||
_LOGGER.debug(
|
||||
"Unexpected response from Windy. Max rentries before disabling resend function: %s",
|
||||
"Unexpected response from Windy. Max retries before disabling resend function: %s",
|
||||
(WINDY_MAX_RETRIES - self.invalid_response_count),
|
||||
)
|
||||
finally:
|
||||
|
|
@ -306,7 +308,7 @@ class WindyPush:
|
|||
WINDY_MAX_RETRIES,
|
||||
)
|
||||
await self._disable_windy(
|
||||
reason="Unable to send data to Windy (3 times). Disabling resend option for now. Please check your Windy configuration and enable this feature afterwards."
|
||||
reason=f"Unable to send data to Windy ({WINDY_MAX_RETRIES} times). Disabling resend option for now. Please check your Windy configuration and enable this feature afterwards."
|
||||
)
|
||||
|
||||
except ClientError as ex:
|
||||
|
|
@ -315,14 +317,16 @@ class WindyPush:
|
|||
# attributes; str(ex) could embed the request URL.
|
||||
self.last_error = type(ex).__name__
|
||||
_LOGGER.critical(
|
||||
"Invalid response from Windy: %s. Will try again later, max rentries before disabling resend function: %s",
|
||||
"Invalid response from Windy: %s. Will try again later, max retries before disabling resend function: %s",
|
||||
str(ex),
|
||||
(WINDY_MAX_RETRIES - self.invalid_response_count),
|
||||
)
|
||||
self.invalid_response_count += 1
|
||||
if self.invalid_response_count >= WINDY_MAX_RETRIES:
|
||||
_LOGGER.critical(WINDY_UNEXPECTED)
|
||||
await self._disable_windy(reason="Invalid response from Windy 3 times. Disabling resending option.")
|
||||
await self._disable_windy(
|
||||
reason=f"Invalid response from Windy {WINDY_MAX_RETRIES} times. Disabling resending option."
|
||||
)
|
||||
self.last_update = dt_util.utcnow()
|
||||
self.next_update = self.last_update + timed(minutes=5)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
from custom_components.sws12500.const import DEFAULT_URL, DOMAIN, WINDY_URL, WSLINK_URL
|
||||
from custom_components.sws12500.const import (
|
||||
DEFAULT_URL,
|
||||
DOMAIN,
|
||||
POCASI_CZ_MAX_RETRIES,
|
||||
POCASI_CZ_UNEXPECTED,
|
||||
WINDY_MAX_RETRIES,
|
||||
WINDY_UNEXPECTED,
|
||||
WINDY_URL,
|
||||
WSLINK_URL,
|
||||
)
|
||||
|
||||
|
||||
def test_const_values():
|
||||
|
|
@ -6,3 +15,9 @@ def test_const_values():
|
|||
assert DEFAULT_URL == "/weatherstation/updateweatherstation.php"
|
||||
assert WSLINK_URL == "/data/upload.php"
|
||||
assert WINDY_URL == "https://stations.windy.com/api/v2/observation/update"
|
||||
|
||||
|
||||
def test_retry_counts_in_user_facing_messages():
|
||||
"""The retry count in the notification texts must follow the constants."""
|
||||
assert f"{WINDY_MAX_RETRIES} times" in WINDY_UNEXPECTED
|
||||
assert f"{POCASI_CZ_MAX_RETRIES} times" in POCASI_CZ_UNEXPECTED
|
||||
|
|
|
|||
|
|
@ -20,8 +20,19 @@ from unittest.mock import AsyncMock, MagicMock
|
|||
import pytest
|
||||
from pytest_homeassistant_custom_component.common import MockConfigEntry
|
||||
|
||||
from custom_components.sws12500 import WeatherDataUpdateCoordinator, async_setup_entry
|
||||
from custom_components.sws12500.const import DOMAIN
|
||||
from custom_components.sws12500 import (
|
||||
CONFIG_ENTRY_VERSION,
|
||||
WeatherDataUpdateCoordinator,
|
||||
async_migrate_entry,
|
||||
async_setup_entry,
|
||||
)
|
||||
from custom_components.sws12500.config_flow import ConfigFlowHandler
|
||||
from custom_components.sws12500.const import (
|
||||
DOMAIN,
|
||||
POCASI_CZ_ENABLED,
|
||||
POCASI_CZ_ENABLED_LEGACY,
|
||||
WINDY_ENABLED,
|
||||
)
|
||||
from custom_components.sws12500.data import SWSRuntimeData
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
|
@ -149,3 +160,83 @@ async def test_check_stale_callback_runs_update(
|
|||
|
||||
captured["cb"](dt_util.utcnow())
|
||||
stale.assert_called_once_with(hass, config_entry)
|
||||
|
||||
|
||||
def test_config_flow_version_matches_migration_target() -> None:
|
||||
"""The flow version and the migration target must not drift apart."""
|
||||
assert ConfigFlowHandler.VERSION == CONFIG_ENTRY_VERSION
|
||||
|
||||
|
||||
async def test_migrate_moves_legacy_pocasi_key(hass) -> None:
|
||||
"""A v1 entry carrying the misspelled key migrates to v2 keeping the value."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={},
|
||||
options={POCASI_CZ_ENABLED_LEGACY: True, WINDY_ENABLED: True},
|
||||
version=1,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await async_migrate_entry(hass, entry) is True
|
||||
|
||||
assert entry.version == 2
|
||||
assert entry.options[POCASI_CZ_ENABLED] is True
|
||||
assert POCASI_CZ_ENABLED_LEGACY not in entry.options
|
||||
# Unrelated options survive untouched.
|
||||
assert entry.options[WINDY_ENABLED] is True
|
||||
|
||||
|
||||
async def test_migrate_without_pocasi_option(hass) -> None:
|
||||
"""An entry that never had the Pocasi option migrates cleanly."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data={}, options={WINDY_ENABLED: False}, version=1)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await async_migrate_entry(hass, entry) is True
|
||||
|
||||
assert entry.version == 2
|
||||
assert POCASI_CZ_ENABLED not in entry.options
|
||||
assert entry.options[WINDY_ENABLED] is False
|
||||
|
||||
|
||||
async def test_migrate_does_not_clobber_correct_key(hass) -> None:
|
||||
"""When both keys are present, the already-correct one wins."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={},
|
||||
options={POCASI_CZ_ENABLED_LEGACY: False, POCASI_CZ_ENABLED: True},
|
||||
version=1,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await async_migrate_entry(hass, entry) is True
|
||||
|
||||
assert entry.options[POCASI_CZ_ENABLED] is True
|
||||
assert POCASI_CZ_ENABLED_LEGACY not in entry.options
|
||||
|
||||
|
||||
async def test_migrate_is_idempotent(hass) -> None:
|
||||
"""Running the migration twice leaves the entry unchanged."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, options={POCASI_CZ_ENABLED_LEGACY: True}, version=1
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await async_migrate_entry(hass, entry) is True
|
||||
first = dict(entry.options)
|
||||
|
||||
assert await async_migrate_entry(hass, entry) is True
|
||||
|
||||
assert entry.version == 2
|
||||
assert dict(entry.options) == first
|
||||
assert entry.options[POCASI_CZ_ENABLED] is True
|
||||
|
||||
|
||||
async def test_migrate_refuses_future_version(hass) -> None:
|
||||
"""An entry written by a newer version is not downgraded."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN, data={}, options={}, version=CONFIG_ENTRY_VERSION + 1
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
assert await async_migrate_entry(hass, entry) is False
|
||||
assert entry.version == CONFIG_ENTRY_VERSION + 1
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@ async def test_push_data_to_windy_missing_station_id_returns_false(monkeypatch,
|
|||
ok = await wp.push_data_to_windy({"a": "b"})
|
||||
assert ok is False
|
||||
assert session.calls == []
|
||||
# Disabling must not overwrite the specific reason the diagnostics sensor reports.
|
||||
assert wp.last_status == "config_error"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -259,6 +261,7 @@ async def test_push_data_to_windy_missing_station_pw_returns_false(monkeypatch,
|
|||
ok = await wp.push_data_to_windy({"a": "b"})
|
||||
assert ok is False
|
||||
assert session.calls == []
|
||||
assert wp.last_status == "config_error"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -288,6 +291,7 @@ async def test_push_data_to_windy_invalid_api_key_disables_windy(monkeypatch, ha
|
|||
ok = await wp.push_data_to_windy({"a": "b"})
|
||||
assert ok is True
|
||||
update_options.assert_awaited_once_with(hass, entry, WINDY_ENABLED, False)
|
||||
assert wp.last_status == "auth_error"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Reference in New Issue