util.py 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
  1. """Small helpers shared between the config flow and entry setup.
  2. Deliberately free of Home Assistant imports so the logic can be unit-tested on
  3. its own.
  4. """
  5. from __future__ import annotations
  6. from typing import Any
  7. try:
  8. from zoneinfo import available_timezones
  9. except ImportError: # pragma: no cover - Python < 3.9, not a supported HA target
  10. available_timezones = None # type: ignore[assignment]
  11. from .const import WHITESPACE_SENSITIVE_FIELDS
  12. def normalise_config_fields(data: dict[str, Any]) -> dict[str, Any]:
  13. """Return a copy of *data* with whitespace trimmed from identifier fields.
  14. The Station ID, Device ID, account name, IOT token and time zone are all
  15. pasted or typed by hand. A trailing space picked up while copying an ID out
  16. of the browser's Network tab makes the upstream API reject the value, and the
  17. user only sees "cannot connect" with no hint as to why.
  18. Which fields are trimmed is defined once in WHITESPACE_SENSITIVE_FIELDS so
  19. the config-flow path and the entry-setup path cannot drift apart. The
  20. password is never trimmed — whitespace there may be intentional.
  21. Non-string values are passed through untouched, so this is safe to call on
  22. a mapping that also carries token-expiry timestamps and similar.
  23. """
  24. cleaned = dict(data)
  25. for key in WHITESPACE_SENSITIVE_FIELDS:
  26. value = cleaned.get(key)
  27. if isinstance(value, str):
  28. cleaned[key] = value.strip()
  29. return cleaned
  30. def is_valid_time_zone(value: str) -> bool:
  31. """Return whether *value* is a real IANA time zone id (e.g. "Europe/Warsaw").
  32. The time-zone field is free text (issue #21): a continent name like
  33. "Europe" or a bare offset reads as plausible to a user copying from a
  34. dropdown elsewhere, but is not a zoneinfo key. Sent as-is in the
  35. IOT-Time-Zone header, the Siseli portal's timeseries endpoint rejects it
  36. outright with "Timeseries error code=20101 message=Illegal argument" —
  37. on the very first API call, with nothing in the error pointing at time
  38. zone as the cause. Catching this locally turns that into a same-form
  39. validation error instead of a cryptic upstream failure during setup.
  40. """
  41. if not value:
  42. return False
  43. if available_timezones is None: # pragma: no cover
  44. return True
  45. return value in available_timezones()