aioesphomeapi 45.2.2__tar.gz → 45.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {aioesphomeapi-45.2.2/aioesphomeapi.egg-info → aioesphomeapi-45.3.1}/PKG-INFO +3 -3
  2. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/client.py +18 -29
  3. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/client_base.py +1 -8
  4. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/connection.py +22 -2
  5. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/model.py +164 -90
  6. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/timezone.py +6 -10
  7. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1/aioesphomeapi.egg-info}/PKG-INFO +3 -3
  8. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi.egg-info/SOURCES.txt +1 -0
  9. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi.egg-info/requires.txt +1 -1
  10. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/pyproject.toml +1 -1
  11. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/requirements/base.txt +1 -1
  12. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/setup.py +1 -1
  13. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_client.py +190 -3
  14. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_connection.py +56 -0
  15. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_model.py +111 -0
  16. aioesphomeapi-45.3.1/tests/test_public_api.py +86 -0
  17. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_timezone.py +77 -20
  18. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/LICENSE +0 -0
  19. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/MANIFEST.in +0 -0
  20. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/README.rst +0 -0
  21. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/__init__.py +0 -0
  22. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/_frame_helper/__init__.py +0 -0
  23. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/_frame_helper/base.py +0 -0
  24. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/_frame_helper/noise.py +0 -0
  25. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/_frame_helper/noise_encryption.py +0 -0
  26. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/_frame_helper/packets.py +0 -0
  27. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/_frame_helper/plain_text.py +0 -0
  28. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/_sanitize.py +0 -0
  29. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/api_options_pb2.py +0 -0
  30. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/api_pb2.py +0 -0
  31. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/ble_defs.py +0 -0
  32. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/core.py +0 -0
  33. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/discover.py +0 -0
  34. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/host_resolver.py +0 -0
  35. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/log_parser.py +0 -0
  36. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/log_reader.py +0 -0
  37. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/log_runner.py +0 -0
  38. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/model_conversions.py +0 -0
  39. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/object_id.py +0 -0
  40. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/posix_tz.py +0 -0
  41. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/py.typed +0 -0
  42. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/reconnect_logic.py +0 -0
  43. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/singleton.py +0 -0
  44. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/state_log_formatter.py +0 -0
  45. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/util.py +0 -0
  46. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi/zeroconf.py +0 -0
  47. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi.egg-info/dependency_links.txt +0 -0
  48. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi.egg-info/entry_points.txt +0 -0
  49. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi.egg-info/not-zip-safe +0 -0
  50. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/aioesphomeapi.egg-info/top_level.txt +0 -0
  51. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/bench/__init__.py +0 -0
  52. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/bench/raw_ble_plain_text.py +0 -0
  53. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/bench/raw_ble_plain_text_with_callback.py +0 -0
  54. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/setup.cfg +0 -0
  55. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test__frame_helper.py +0 -0
  56. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test__sanitize.py +0 -0
  57. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_core.py +0 -0
  58. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_discover.py +0 -0
  59. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_host_resolver.py +0 -0
  60. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_log_parser.py +0 -0
  61. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_log_reader.py +0 -0
  62. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_log_runner.py +0 -0
  63. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_model_conversions.py +0 -0
  64. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_object_id.py +0 -0
  65. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_posix_tz.py +0 -0
  66. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_provide_time.py +0 -0
  67. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_reconnect_logic.py +0 -0
  68. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_singleton.py +0 -0
  69. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_state_log_formatter.py +0 -0
  70. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_util.py +0 -0
  71. {aioesphomeapi-45.2.2 → aioesphomeapi-45.3.1}/tests/test_zeroconf.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioesphomeapi
3
- Version: 45.2.2
3
+ Version: 45.3.1
4
4
  Summary: Python API for interacting with ESPHome devices.
5
5
  Home-page: https://esphome.io/
6
- Download-URL: https://github.com/esphome/aioesphomeapi/archive/45.2.2.zip
6
+ Download-URL: https://github.com/esphome/aioesphomeapi/archive/45.3.1.zip
7
7
  Author: Otto Winter
8
8
  Author-email: esphome@nabucasa.com
9
9
  License: MIT
@@ -12,7 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: aiohappyeyeballs>=2.6.2
13
13
  Requires-Dist: async-interrupt>=1.2.2
14
14
  Requires-Dist: protobuf<8,>=6
15
- Requires-Dist: tzdata>=2024.2
15
+ Requires-Dist: tzdata>=2026.2
16
16
  Requires-Dist: tzlocal<6,>=5.3.1
17
17
  Requires-Dist: zeroconf<2.0,>=0.149.16
18
18
  Requires-Dist: chacha20poly1305-reuseable>=0.13.2
@@ -859,7 +859,7 @@ class APIClient(APIClientBase):
859
859
  """Set the Bluetooth scanner mode."""
860
860
  self._get_connection().send_message(BluetoothScannerSetModeRequest(mode=mode))
861
861
 
862
- async def bluetooth_device_connect( # noqa: C901 # pylint: disable=too-many-locals, too-many-branches
862
+ async def bluetooth_device_connect( # noqa: C901 # pylint: disable=too-many-locals
863
863
  self,
864
864
  address: int,
865
865
  on_bluetooth_connection_state: Callable[[bool, int, int], None],
@@ -923,22 +923,16 @@ class APIClient(APIClientBase):
923
923
  timeout_handle = loop.call_at(
924
924
  loop.time() + timeout, handle_timeout, connect_future
925
925
  )
926
- timeout_expired = False
927
- connect_ok = False
928
- unhandled_exception = False
929
926
  try:
930
927
  await connect_future
931
- connect_ok = True
932
928
  except TimeoutError as err:
933
- # If the timeout expires, make sure
934
- # to unsub before calling _bluetooth_device_disconnect_guard_timeout
935
- # so that the disconnect message is not propagated back to the caller
936
- # since we are going to raise a TimeoutAPIError.
929
+ # Unsub before disconnecting so the disconnect message is not
930
+ # propagated back to the caller we are going to raise a
931
+ # TimeoutAPIError instead.
937
932
  unsub()
938
- timeout_expired = True
939
- # Disconnect before raising the exception to ensure
940
- # the slot is recovered before the timeout is raised
941
- # to avoid race were we run out even though we have a slot.
933
+ # Disconnect before raising the exception so the slot is
934
+ # recovered before the timeout is raised, avoiding a race
935
+ # where we run out of slots even though we have one.
942
936
  addr = to_human_readable_address(address)
943
937
  if self._debug_enabled:
944
938
  _LOGGER.debug("%s: Connecting timed out, waiting for disconnect", addr)
@@ -954,13 +948,14 @@ class APIClient(APIClientBase):
954
948
  )
955
949
  raise TimeoutAPIError(msg) from err
956
950
  except asyncio.CancelledError:
957
- unhandled_exception = True
958
- # Distinguish an outside cancellation of our task from
959
- # a cancellation of the connect_future itself. If the
960
- # current task is not actually being cancelled, convert
961
- # the CancelledError into an APIConnectionError so that
962
- # callers (and their retry logic) can handle it as a
963
- # normal connection failure instead of aborting.
951
+ unsub()
952
+ self._bluetooth_disconnect_no_wait(address)
953
+ # Distinguish an outside cancellation of our task from a
954
+ # cancellation of the connect_future itself. If the current
955
+ # task is not actually being cancelled, convert the
956
+ # CancelledError into an APIConnectionError so callers (and
957
+ # their retry logic) can treat it as a normal connection
958
+ # failure instead of aborting.
964
959
  current_task = asyncio.current_task()
965
960
  if current_task is None or not current_task.cancelling():
966
961
  addr = to_human_readable_address(address)
@@ -968,17 +963,11 @@ class APIClient(APIClientBase):
968
963
  raise APIConnectionError(msg) from None
969
964
  raise
970
965
  except BaseException:
971
- unhandled_exception = True
966
+ unsub()
967
+ self._bluetooth_disconnect_no_wait(address)
972
968
  raise
973
969
  finally:
974
- if unhandled_exception or (not connect_ok and not timeout_expired):
975
- unsub()
976
- if not timeout_expired:
977
- timeout_handle.cancel()
978
- if unhandled_exception:
979
- # Make sure to disconnect if we had an unhandled exception
980
- # as otherwise the connection will be left open.
981
- self._bluetooth_disconnect_no_wait(address)
970
+ timeout_handle.cancel()
982
971
 
983
972
  return unsub
984
973
 
@@ -166,14 +166,7 @@ def on_bluetooth_gatt_notify_data_response(
166
166
  ) -> None:
167
167
  """Handle a BluetoothGATTNotifyDataResponse message."""
168
168
  if address == msg.address and handle == msg.handle:
169
- try:
170
- on_bluetooth_gatt_notify(handle, bytearray(msg.data))
171
- except Exception:
172
- _LOGGER.exception(
173
- "Unexpected error in Bluetooth GATT notify callback for address %s, handle %s",
174
- address,
175
- handle,
176
- )
169
+ on_bluetooth_gatt_notify(handle, bytearray(msg.data))
177
170
 
178
171
 
179
172
  def on_bluetooth_scanner_state_response(
@@ -1121,7 +1121,19 @@ class APIConnection:
1121
1121
  # type.
1122
1122
  handlers_copy = handlers.copy()
1123
1123
  for handler in handlers_copy:
1124
- handler(msg)
1124
+ # Isolate user-callback exceptions so a buggy
1125
+ # handler does not propagate through asyncio's
1126
+ # data_received path and tear the whole session
1127
+ # down. See issue #1755.
1128
+ try:
1129
+ handler(msg)
1130
+ except Exception:
1131
+ _LOGGER.exception(
1132
+ "%s: Unexpected error in message handler %r for %s",
1133
+ self.log_name,
1134
+ handler,
1135
+ type(msg).__name__,
1136
+ )
1125
1137
  return
1126
1138
 
1127
1139
  # Most common case, only one handler:
@@ -1130,7 +1142,15 @@ class APIConnection:
1130
1142
  # only one handler because Cython will
1131
1143
  # poorly optimize next(iter(handlers))
1132
1144
  for handler in handlers:
1133
- handler(msg)
1145
+ try:
1146
+ handler(msg)
1147
+ except Exception:
1148
+ _LOGGER.exception(
1149
+ "%s: Unexpected error in message handler %r for %s",
1150
+ self.log_name,
1151
+ handler,
1152
+ type(msg).__name__,
1153
+ )
1134
1154
  break
1135
1155
 
1136
1156
  def _register_internal_message_handlers(self) -> None:
@@ -84,6 +84,12 @@ class APIModelBase:
84
84
  def from_pb(cls, data: Any) -> Self:
85
85
  return cls(**{f.name: getattr(data, f.name) for f in cached_fields(cls)}) # type: ignore[arg-type]
86
86
 
87
+ @classmethod
88
+ def convert_list(cls, value: list[Any]) -> list[Self]:
89
+ return [
90
+ cls.from_dict(x) if isinstance(x, dict) else cls.from_pb(x) for x in value
91
+ ]
92
+
87
93
 
88
94
  def converter_field(*, converter: Callable[[Any], _V], **kwargs: Any) -> _V:
89
95
  metadata = kwargs.pop("metadata", {})
@@ -185,16 +191,6 @@ class AreaInfo(APIModelBase):
185
191
  area_id: int = 0
186
192
  name: str = ""
187
193
 
188
- @classmethod
189
- def convert_list(cls, value: list[Any]) -> list[AreaInfo]:
190
- ret = []
191
- for x in value:
192
- if isinstance(x, dict):
193
- ret.append(AreaInfo.from_dict(x))
194
- else:
195
- ret.append(AreaInfo.from_pb(x))
196
- return ret
197
-
198
194
  @classmethod
199
195
  def convert(cls, value: Any) -> AreaInfo:
200
196
  if isinstance(value, dict):
@@ -208,16 +204,6 @@ class SubDeviceInfo(APIModelBase):
208
204
  name: str = ""
209
205
  area_id: int = 0
210
206
 
211
- @classmethod
212
- def convert_list(cls, value: list[Any]) -> list[SubDeviceInfo]:
213
- ret = []
214
- for x in value:
215
- if isinstance(x, dict):
216
- ret.append(SubDeviceInfo.from_dict(x))
217
- else:
218
- ret.append(SubDeviceInfo.from_pb(x))
219
- return ret
220
-
221
207
 
222
208
  class SerialProxyPortType(APIIntEnum):
223
209
  TTL = 0
@@ -232,16 +218,6 @@ class SerialProxyInfo(APIModelBase):
232
218
  default=SerialProxyPortType.TTL, converter=SerialProxyPortType.convert
233
219
  )
234
220
 
235
- @classmethod
236
- def convert_list(cls, value: list[Any]) -> list[SerialProxyInfo]:
237
- ret = []
238
- for x in value:
239
- if isinstance(x, dict):
240
- ret.append(SerialProxyInfo.from_dict(x))
241
- else:
242
- ret.append(SerialProxyInfo.from_pb(x))
243
- return ret
244
-
245
221
 
246
222
  @_frozen_dataclass_decorator
247
223
  class DeviceInfo(APIModelBase):
@@ -1061,16 +1037,6 @@ class MediaPlayerSupportedFormat(APIModelBase):
1061
1037
  )
1062
1038
  sample_bytes: int = 0
1063
1039
 
1064
- @classmethod
1065
- def convert_list(cls, value: list[Any]) -> list[MediaPlayerSupportedFormat]:
1066
- ret = []
1067
- for x in value:
1068
- if isinstance(x, dict):
1069
- ret.append(MediaPlayerSupportedFormat.from_dict(x))
1070
- else:
1071
- ret.append(MediaPlayerSupportedFormat.from_pb(x))
1072
- return ret
1073
-
1074
1040
 
1075
1041
  @_frozen_dataclass_decorator
1076
1042
  class MediaPlayerInfo(EntityInfo):
@@ -1668,16 +1634,6 @@ class BluetoothGATTDescriptor(APIModelBase):
1668
1634
  data["uuid"] = _join_split_uuid(data["uuid"])
1669
1635
  return APIModelBase.from_dict.__func__(cls, data, ignore_missing=ignore_missing) # type: ignore[attr-defined, no-any-return]
1670
1636
 
1671
- @classmethod
1672
- def convert_list(cls, value: list[Any]) -> list[BluetoothGATTDescriptor]:
1673
- ret = []
1674
- for x in value:
1675
- if isinstance(x, dict):
1676
- ret.append(cls.from_dict(x))
1677
- else:
1678
- ret.append(cls.from_pb(x))
1679
- return ret
1680
-
1681
1637
 
1682
1638
  @_frozen_dataclass_decorator
1683
1639
  class BluetoothGATTCharacteristic(APIModelBase):
@@ -1713,16 +1669,6 @@ class BluetoothGATTCharacteristic(APIModelBase):
1713
1669
  data["uuid"] = _join_split_uuid(data["uuid"])
1714
1670
  return APIModelBase.from_dict.__func__(cls, data, ignore_missing=ignore_missing) # type: ignore[attr-defined, no-any-return]
1715
1671
 
1716
- @classmethod
1717
- def convert_list(cls, value: list[Any]) -> list[BluetoothGATTCharacteristic]:
1718
- ret = []
1719
- for x in value:
1720
- if isinstance(x, dict):
1721
- ret.append(cls.from_dict(x))
1722
- else:
1723
- ret.append(cls.from_pb(x))
1724
- return ret
1725
-
1726
1672
 
1727
1673
  @_frozen_dataclass_decorator
1728
1674
  class BluetoothGATTService(APIModelBase):
@@ -1757,16 +1703,6 @@ class BluetoothGATTService(APIModelBase):
1757
1703
  data["uuid"] = _join_split_uuid(data["uuid"])
1758
1704
  return APIModelBase.from_dict.__func__(cls, data, ignore_missing=ignore_missing) # type: ignore[attr-defined, no-any-return]
1759
1705
 
1760
- @classmethod
1761
- def convert_list(cls, value: list[Any]) -> list[BluetoothGATTService]:
1762
- ret = []
1763
- for x in value:
1764
- if isinstance(x, dict):
1765
- ret.append(cls.from_dict(x))
1766
- else:
1767
- ret.append(cls.from_pb(x))
1768
- return ret
1769
-
1770
1706
 
1771
1707
  @_frozen_dataclass_decorator
1772
1708
  class BluetoothGATTServices(APIModelBase):
@@ -1877,16 +1813,6 @@ class VoiceAssistantWakeWord(APIModelBase):
1877
1813
  wake_word: str
1878
1814
  trained_languages: list[str]
1879
1815
 
1880
- @classmethod
1881
- def convert_list(cls, value: list[Any]) -> list[VoiceAssistantWakeWord]:
1882
- ret = []
1883
- for x in value:
1884
- if isinstance(x, dict):
1885
- ret.append(VoiceAssistantWakeWord.from_dict(x))
1886
- else:
1887
- ret.append(VoiceAssistantWakeWord.from_pb(x))
1888
- return ret
1889
-
1890
1816
 
1891
1817
  @_frozen_dataclass_decorator
1892
1818
  class VoiceAssistantExternalWakeWord(APIModelBase):
@@ -1898,16 +1824,6 @@ class VoiceAssistantExternalWakeWord(APIModelBase):
1898
1824
  model_hash: str
1899
1825
  url: str
1900
1826
 
1901
- @classmethod
1902
- def convert_list(cls, value: list[Any]) -> list[VoiceAssistantExternalWakeWord]:
1903
- ret = []
1904
- for x in value:
1905
- if isinstance(x, dict):
1906
- ret.append(VoiceAssistantExternalWakeWord.from_dict(x))
1907
- else:
1908
- ret.append(VoiceAssistantExternalWakeWord.from_pb(x))
1909
- return ret
1910
-
1911
1827
 
1912
1828
  @_frozen_dataclass_decorator
1913
1829
  class VoiceAssistantConfigurationResponse(APIModelBase):
@@ -2066,3 +1982,161 @@ def build_unique_id(
2066
1982
 
2067
1983
  def message_types_to_names(msg_types: Iterable[type[message.Message]]) -> str:
2068
1984
  return ", ".join(t.__name__ for t in msg_types)
1985
+
1986
+
1987
+ __all__ = (
1988
+ "COMPONENT_TYPE_TO_INFO",
1989
+ "APIIntEnum",
1990
+ "APIModelBase",
1991
+ "APIVersion",
1992
+ "AlarmControlPanelCommand",
1993
+ "AlarmControlPanelEntityFeature",
1994
+ "AlarmControlPanelEntityState",
1995
+ "AlarmControlPanelInfo",
1996
+ "AlarmControlPanelState",
1997
+ "AreaInfo",
1998
+ "BinarySensorInfo",
1999
+ "BinarySensorState",
2000
+ "BluetoothConnectionsFree",
2001
+ "BluetoothDeviceClearCache",
2002
+ "BluetoothDeviceConnection",
2003
+ "BluetoothDevicePairing",
2004
+ "BluetoothDeviceRequestType",
2005
+ "BluetoothDeviceUnpairing",
2006
+ "BluetoothGATTCharacteristic",
2007
+ "BluetoothGATTDescriptor",
2008
+ "BluetoothGATTError",
2009
+ "BluetoothGATTRead",
2010
+ "BluetoothGATTService",
2011
+ "BluetoothGATTServices",
2012
+ "BluetoothLEAdvertisement",
2013
+ "BluetoothProxyFeature",
2014
+ "BluetoothProxySubscriptionFlag",
2015
+ "BluetoothScannerMode",
2016
+ "BluetoothScannerState",
2017
+ "BluetoothScannerStateResponse",
2018
+ "ButtonInfo",
2019
+ "CameraInfo",
2020
+ "CameraState",
2021
+ "ClimateAction",
2022
+ "ClimateFanMode",
2023
+ "ClimateFeature",
2024
+ "ClimateInfo",
2025
+ "ClimateMode",
2026
+ "ClimatePreset",
2027
+ "ClimateState",
2028
+ "ClimateSwingMode",
2029
+ "ColorMode",
2030
+ "CommandProtoMessage",
2031
+ "CoverInfo",
2032
+ "CoverOperation",
2033
+ "CoverState",
2034
+ "DateInfo",
2035
+ "DateState",
2036
+ "DateTimeInfo",
2037
+ "DateTimeState",
2038
+ "DeviceInfo",
2039
+ "ESPHomeBluetoothGATTServices",
2040
+ "EntityCategory",
2041
+ "EntityInfo",
2042
+ "EntityState",
2043
+ "Event",
2044
+ "EventInfo",
2045
+ "ExecuteServiceResponse",
2046
+ "FanDirection",
2047
+ "FanInfo",
2048
+ "FanSpeed",
2049
+ "FanState",
2050
+ "HomeassistantActionResponse",
2051
+ "HomeassistantServiceCall",
2052
+ "InfraredCapability",
2053
+ "InfraredInfo",
2054
+ "InfraredRFReceiveEvent",
2055
+ "LastResetType",
2056
+ "LegacyCoverCommand",
2057
+ "LegacyCoverState",
2058
+ "LightColorCapability",
2059
+ "LightInfo",
2060
+ "LightState",
2061
+ "LockCommand",
2062
+ "LockEntityState",
2063
+ "LockInfo",
2064
+ "LockState",
2065
+ "LogLevel",
2066
+ "MediaPlayerCommand",
2067
+ "MediaPlayerEntityFeature",
2068
+ "MediaPlayerEntityState",
2069
+ "MediaPlayerFormatPurpose",
2070
+ "MediaPlayerInfo",
2071
+ "MediaPlayerState",
2072
+ "MediaPlayerSupportedFormat",
2073
+ "NoiseEncryptionSetKeyRequest",
2074
+ "NoiseEncryptionSetKeyResponse",
2075
+ "NumberInfo",
2076
+ "NumberMode",
2077
+ "NumberState",
2078
+ "RadioFrequencyCapability",
2079
+ "RadioFrequencyInfo",
2080
+ "RadioFrequencyModulation",
2081
+ "SelectInfo",
2082
+ "SelectState",
2083
+ "SensorInfo",
2084
+ "SensorState",
2085
+ "SensorStateClass",
2086
+ "SerialProxyDataReceived",
2087
+ "SerialProxyInfo",
2088
+ "SerialProxyModemPins",
2089
+ "SerialProxyParity",
2090
+ "SerialProxyPortType",
2091
+ "SerialProxyRequestResponse",
2092
+ "SerialProxyRequestType",
2093
+ "SerialProxyStatus",
2094
+ "SirenInfo",
2095
+ "SirenState",
2096
+ "SubDeviceInfo",
2097
+ "SupportsResponseType",
2098
+ "SwitchInfo",
2099
+ "SwitchState",
2100
+ "TemperatureUnit",
2101
+ "TextInfo",
2102
+ "TextMode",
2103
+ "TextSensorInfo",
2104
+ "TextSensorState",
2105
+ "TextState",
2106
+ "TimeInfo",
2107
+ "TimeState",
2108
+ "UpdateCommand",
2109
+ "UpdateInfo",
2110
+ "UpdateState",
2111
+ "UserService",
2112
+ "UserServiceArg",
2113
+ "UserServiceArgType",
2114
+ "ValveInfo",
2115
+ "ValveOperation",
2116
+ "ValveState",
2117
+ "VoiceAssistantAnnounceFinished",
2118
+ "VoiceAssistantAudioData",
2119
+ "VoiceAssistantAudioSettings",
2120
+ "VoiceAssistantCommand",
2121
+ "VoiceAssistantCommandFlag",
2122
+ "VoiceAssistantConfigurationRequest",
2123
+ "VoiceAssistantConfigurationResponse",
2124
+ "VoiceAssistantEventType",
2125
+ "VoiceAssistantExternalWakeWord",
2126
+ "VoiceAssistantFeature",
2127
+ "VoiceAssistantSetConfiguration",
2128
+ "VoiceAssistantSubscriptionFlag",
2129
+ "VoiceAssistantTimerEventType",
2130
+ "VoiceAssistantWakeWord",
2131
+ "WaterHeaterCommandField",
2132
+ "WaterHeaterFeature",
2133
+ "WaterHeaterInfo",
2134
+ "WaterHeaterMode",
2135
+ "WaterHeaterState",
2136
+ "WaterHeaterStateFlag",
2137
+ "ZWaveProxyFeature",
2138
+ "ZWaveProxyFrame",
2139
+ "ZWaveProxyRequest",
2140
+ "ZWaveProxyRequestType",
2141
+ "build_unique_id",
2142
+ )
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
- from functools import cache
6
+ from functools import cache, lru_cache
7
7
  from importlib import resources
8
8
  import logging
9
9
 
@@ -71,6 +71,7 @@ def _get_local_timezone() -> str:
71
71
  return ""
72
72
 
73
73
 
74
+ @lru_cache(maxsize=64)
74
75
  def iana_to_posix_tz(iana_key: str) -> str:
75
76
  """Convert IANA timezone key to POSIX TZ string.
76
77
 
@@ -115,12 +116,7 @@ async def get_timezone(iana_key: str | None) -> str:
115
116
  Returns empty string if timezone cannot be determined.
116
117
 
117
118
  """
118
- if iana_key:
119
-
120
- @singleton(f"get_timezone_{iana_key}")
121
- async def _get_iana_timezone() -> str:
122
- loop = asyncio.get_running_loop()
123
- return await loop.run_in_executor(None, iana_to_posix_tz, iana_key)
124
-
125
- return await _get_iana_timezone()
126
- return await get_local_timezone()
119
+ if not iana_key:
120
+ return await get_local_timezone()
121
+ loop = asyncio.get_running_loop()
122
+ return await loop.run_in_executor(None, iana_to_posix_tz, iana_key)
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioesphomeapi
3
- Version: 45.2.2
3
+ Version: 45.3.1
4
4
  Summary: Python API for interacting with ESPHome devices.
5
5
  Home-page: https://esphome.io/
6
- Download-URL: https://github.com/esphome/aioesphomeapi/archive/45.2.2.zip
6
+ Download-URL: https://github.com/esphome/aioesphomeapi/archive/45.3.1.zip
7
7
  Author: Otto Winter
8
8
  Author-email: esphome@nabucasa.com
9
9
  License: MIT
@@ -12,7 +12,7 @@ License-File: LICENSE
12
12
  Requires-Dist: aiohappyeyeballs>=2.6.2
13
13
  Requires-Dist: async-interrupt>=1.2.2
14
14
  Requires-Dist: protobuf<8,>=6
15
- Requires-Dist: tzdata>=2024.2
15
+ Requires-Dist: tzdata>=2026.2
16
16
  Requires-Dist: tzlocal<6,>=5.3.1
17
17
  Requires-Dist: zeroconf<2.0,>=0.149.16
18
18
  Requires-Dist: chacha20poly1305-reuseable>=0.13.2
@@ -61,6 +61,7 @@ tests/test_model_conversions.py
61
61
  tests/test_object_id.py
62
62
  tests/test_posix_tz.py
63
63
  tests/test_provide_time.py
64
+ tests/test_public_api.py
64
65
  tests/test_reconnect_logic.py
65
66
  tests/test_singleton.py
66
67
  tests/test_state_log_formatter.py
@@ -1,7 +1,7 @@
1
1
  aiohappyeyeballs>=2.6.2
2
2
  async-interrupt>=1.2.2
3
3
  protobuf<8,>=6
4
- tzdata>=2024.2
4
+ tzdata>=2026.2
5
5
  tzlocal<6,>=5.3.1
6
6
  zeroconf<2.0,>=0.149.16
7
7
  chacha20poly1305-reuseable>=0.13.2
@@ -128,7 +128,7 @@ combine-as-imports = true
128
128
  split-on-trailing-comma = false
129
129
 
130
130
  [build-system]
131
- requires = ['setuptools>=82.0.1', 'wheel', 'Cython>=3.2.4']
131
+ requires = ['setuptools>=82.0.1', 'wheel', 'Cython>=3.2.5']
132
132
 
133
133
  [tool.pytest.ini_options]
134
134
  asyncio_mode = "auto"
@@ -1,7 +1,7 @@
1
1
  aiohappyeyeballs>=2.6.2
2
2
  async-interrupt>=1.2.2
3
3
  protobuf>=6,<8
4
- tzdata>=2024.2
4
+ tzdata>=2026.2
5
5
  tzlocal>=5.3.1,<6
6
6
  zeroconf>=0.149.16,<2.0
7
7
  chacha20poly1305-reuseable>=0.13.2
@@ -42,7 +42,7 @@ with (here / "README.rst").open(encoding="utf-8") as readme_file:
42
42
  long_description = readme_file.read()
43
43
 
44
44
 
45
- VERSION = "45.2.2"
45
+ VERSION = "45.3.1"
46
46
  PROJECT_NAME = "aioesphomeapi"
47
47
  PROJECT_PACKAGE_NAME = "aioesphomeapi"
48
48
  PROJECT_LICENSE = "MIT"