esphome 2025.2.1__py3-none-any.whl → 2025.3.0b1__py3-none-any.whl

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 (140) hide show
  1. esphome/__main__.py +9 -1
  2. esphome/components/api/api_connection.cpp +426 -70
  3. esphome/components/api/api_connection.h +117 -25
  4. esphome/components/api/api_pb2.cpp +9 -0
  5. esphome/components/api/api_pb2.h +1 -0
  6. esphome/components/api/api_server.cpp +2 -2
  7. esphome/components/api/list_entities.cpp +76 -22
  8. esphome/components/api/list_entities.h +1 -0
  9. esphome/components/api/subscribe_state.h +2 -0
  10. esphome/components/audio/audio_reader.cpp +7 -7
  11. esphome/components/audio/audio_reader.h +1 -1
  12. esphome/components/bluetooth_proxy/bluetooth_proxy.h +8 -0
  13. esphome/components/bmp085/bmp085.cpp +1 -1
  14. esphome/components/chsc6x/__init__.py +2 -0
  15. esphome/components/chsc6x/chsc6x_touchscreen.cpp +47 -0
  16. esphome/components/chsc6x/chsc6x_touchscreen.h +34 -0
  17. esphome/components/chsc6x/touchscreen.py +33 -0
  18. esphome/components/climate/__init__.py +0 -1
  19. esphome/components/cst816/binary_sensor/__init__.py +2 -25
  20. esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +3 -14
  21. esphome/components/cst816/touchscreen/cst816_touchscreen.h +0 -4
  22. esphome/components/esp32_ble_beacon/__init__.py +3 -1
  23. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +2 -2
  24. esphome/components/esp8266/gpio.py +1 -2
  25. esphome/components/font/__init__.py +185 -185
  26. esphome/components/font/font.cpp +4 -4
  27. esphome/components/font/font.h +1 -0
  28. esphome/components/haier/climate.py +11 -10
  29. esphome/components/hbridge/switch/hbridge_switch.cpp +2 -2
  30. esphome/components/heatpumpir/climate.py +2 -1
  31. esphome/components/heatpumpir/heatpumpir.cpp +1 -0
  32. esphome/components/heatpumpir/heatpumpir.h +1 -0
  33. esphome/components/i2c/__init__.py +6 -6
  34. esphome/components/i2c/i2c_bus_esp_idf.cpp +6 -2
  35. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
  36. esphome/components/ili9xxx/display.py +1 -0
  37. esphome/components/ili9xxx/ili9xxx_display.h +5 -0
  38. esphome/components/ili9xxx/ili9xxx_init.h +59 -0
  39. esphome/components/ld2450/__init__.py +51 -0
  40. esphome/components/ld2450/binary_sensor.py +47 -0
  41. esphome/components/ld2450/button/__init__.py +45 -0
  42. esphome/components/ld2450/button/reset_button.cpp +9 -0
  43. esphome/components/ld2450/button/reset_button.h +18 -0
  44. esphome/components/ld2450/button/restart_button.cpp +9 -0
  45. esphome/components/ld2450/button/restart_button.h +18 -0
  46. esphome/components/ld2450/ld2450.cpp +876 -0
  47. esphome/components/ld2450/ld2450.h +234 -0
  48. esphome/components/ld2450/number/__init__.py +121 -0
  49. esphome/components/ld2450/number/presence_timeout_number.cpp +12 -0
  50. esphome/components/ld2450/number/presence_timeout_number.h +18 -0
  51. esphome/components/ld2450/number/zone_coordinate_number.cpp +14 -0
  52. esphome/components/ld2450/number/zone_coordinate_number.h +19 -0
  53. esphome/components/ld2450/select/__init__.py +56 -0
  54. esphome/components/ld2450/select/baud_rate_select.cpp +12 -0
  55. esphome/components/ld2450/select/baud_rate_select.h +18 -0
  56. esphome/components/ld2450/select/zone_type_select.cpp +12 -0
  57. esphome/components/ld2450/select/zone_type_select.h +18 -0
  58. esphome/components/ld2450/sensor.py +156 -0
  59. esphome/components/ld2450/switch/__init__.py +45 -0
  60. esphome/components/ld2450/switch/bluetooth_switch.cpp +12 -0
  61. esphome/components/ld2450/switch/bluetooth_switch.h +18 -0
  62. esphome/components/ld2450/switch/multi_target_switch.cpp +12 -0
  63. esphome/components/ld2450/switch/multi_target_switch.h +18 -0
  64. esphome/components/ld2450/text_sensor.py +62 -0
  65. esphome/components/ltr390/ltr390.cpp +7 -7
  66. esphome/components/ltr390/ltr390.h +0 -1
  67. esphome/components/lvgl/defines.py +0 -2
  68. esphome/components/lvgl/font.cpp +1 -1
  69. esphome/components/lvgl/lvgl_esphome.cpp +27 -19
  70. esphome/components/lvgl/widgets/img.py +1 -3
  71. esphome/components/mcp2515/mcp2515.cpp +1 -0
  72. esphome/components/mlx90393/sensor.py +53 -33
  73. esphome/components/mlx90393/sensor_mlx90393.cpp +4 -0
  74. esphome/components/mlx90393/sensor_mlx90393.h +8 -3
  75. esphome/components/mqtt/__init__.py +2 -2
  76. esphome/components/msa3xx/__init__.py +189 -0
  77. esphome/components/msa3xx/binary_sensor.py +40 -0
  78. esphome/components/msa3xx/msa3xx.cpp +417 -0
  79. esphome/components/msa3xx/msa3xx.h +311 -0
  80. esphome/components/msa3xx/sensor.py +42 -0
  81. esphome/components/msa3xx/text_sensor.py +38 -0
  82. esphome/components/nfc/binary_sensor/__init__.py +4 -4
  83. esphome/components/opentherm/binary_sensor/__init__.py +4 -4
  84. esphome/components/opentherm/generate.py +6 -6
  85. esphome/components/opentherm/sensor/__init__.py +5 -6
  86. esphome/components/packages/__init__.py +35 -11
  87. esphome/components/pn532/binary_sensor.py +4 -4
  88. esphome/components/rc522/binary_sensor.py +4 -4
  89. esphome/components/socket/bsd_sockets_impl.cpp +1 -0
  90. esphome/components/socket/lwip_sockets_impl.cpp +1 -0
  91. esphome/components/socket/socket.h +3 -1
  92. esphome/components/ssd1306_base/__init__.py +7 -7
  93. esphome/components/thermostat/climate.py +1 -1
  94. esphome/components/tmp1075/tmp1075.cpp +7 -11
  95. esphome/components/tmp1075/tmp1075.h +1 -2
  96. esphome/components/tormatic/__init__.py +1 -0
  97. esphome/components/tormatic/cover.py +47 -0
  98. esphome/components/tormatic/tormatic_cover.cpp +355 -0
  99. esphome/components/tormatic/tormatic_cover.h +60 -0
  100. esphome/components/tormatic/tormatic_protocol.h +211 -0
  101. esphome/components/touchscreen/binary_sensor/__init__.py +3 -0
  102. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +7 -1
  103. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +3 -1
  104. esphome/components/touchscreen/touchscreen.cpp +3 -4
  105. esphome/components/udp/udp_component.h +4 -1
  106. esphome/components/web_server/list_entities.cpp +70 -66
  107. esphome/components/web_server/list_entities.h +43 -22
  108. esphome/components/web_server/web_server.cpp +345 -68
  109. esphome/components/web_server/web_server.h +138 -6
  110. esphome/components/web_server_base/__init__.py +1 -1
  111. esphome/components/web_server_idf/__init__.py +2 -0
  112. esphome/components/web_server_idf/web_server_idf.cpp +177 -30
  113. esphome/components/web_server_idf/web_server_idf.h +53 -4
  114. esphome/config_validation.py +23 -125
  115. esphome/const.py +5 -1
  116. esphome/core/config.py +12 -4
  117. esphome/core/defines.h +1 -1
  118. esphome/core/helpers.h +5 -3
  119. esphome/core/time.cpp +1 -0
  120. esphome/cpp_generator.py +3 -3
  121. esphome/dashboard/core.py +30 -21
  122. esphome/dashboard/dns.py +7 -1
  123. esphome/dashboard/entries.py +83 -16
  124. esphome/dashboard/settings.py +0 -4
  125. esphome/dashboard/status/mdns.py +43 -14
  126. esphome/dashboard/status/mqtt.py +22 -9
  127. esphome/dashboard/status/ping.py +54 -10
  128. esphome/dashboard/web_server.py +56 -24
  129. esphome/storage_json.py +4 -0
  130. esphome/wizard.py +13 -17
  131. esphome/writer.py +1 -3
  132. esphome/yaml_util.py +36 -33
  133. esphome/zeroconf.py +9 -21
  134. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/METADATA +5 -5
  135. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/RECORD +139 -99
  136. esphome/components/cst816/binary_sensor/cst816_button.h +0 -27
  137. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/LICENSE +0 -0
  138. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/WHEEL +0 -0
  139. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/entry_points.txt +0 -0
  140. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ from collections.abc import MutableMapping
1
2
  import functools
2
3
  import hashlib
3
4
  import logging
@@ -6,10 +7,10 @@ from pathlib import Path
6
7
  import re
7
8
 
8
9
  import esphome_glyphsets as glyphsets
9
- import freetype
10
+ from freetype import Face, ft_pixel_mode_grays, ft_pixel_mode_mono
10
11
  import requests
11
12
 
12
- from esphome import core, external_files
13
+ from esphome import external_files
13
14
  import esphome.codegen as cg
14
15
  import esphome.config_validation as cv
15
16
  from esphome.const import (
@@ -26,7 +27,7 @@ from esphome.const import (
26
27
  CONF_WEIGHT,
27
28
  )
28
29
  from esphome.core import CORE, HexInt
29
- from esphome.helpers import copy_file_if_changed, cpp_string_escape
30
+ from esphome.helpers import cpp_string_escape
30
31
 
31
32
  _LOGGER = logging.getLogger(__name__)
32
33
 
@@ -49,13 +50,42 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
49
50
 
50
51
 
51
52
  # Cache loaded freetype fonts
52
- class FontCache(dict):
53
- def __missing__(self, key):
54
- try:
55
- res = self[key] = freetype.Face(key)
56
- return res
57
- except freetype.FT_Exception as e:
58
- raise cv.Invalid(f"Could not load Font file {key}: {e}") from e
53
+ class FontCache(MutableMapping):
54
+ @staticmethod
55
+ def get_name(value):
56
+ if CONF_FAMILY in value:
57
+ return (
58
+ f"{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}"
59
+ )
60
+ if CONF_URL in value:
61
+ return value[CONF_URL]
62
+ return value[CONF_PATH]
63
+
64
+ @staticmethod
65
+ def _keytransform(value):
66
+ if CONF_FAMILY in value:
67
+ return f"gfont:{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}"
68
+ if CONF_URL in value:
69
+ return f"url:{value[CONF_URL]}"
70
+ return f"file:{value[CONF_PATH]}"
71
+
72
+ def __init__(self):
73
+ self.store = {}
74
+
75
+ def __delitem__(self, key):
76
+ del self.store[self._keytransform(key)]
77
+
78
+ def __iter__(self):
79
+ return iter(self.store)
80
+
81
+ def __len__(self):
82
+ return len(self.store)
83
+
84
+ def __getitem__(self, item):
85
+ return self.store[self._keytransform(item)]
86
+
87
+ def __setitem__(self, key, value):
88
+ self.store[self._keytransform(key)] = Face(str(value))
59
89
 
60
90
 
61
91
  FONT_CACHE = FontCache()
@@ -109,14 +139,14 @@ def check_missing_glyphs(file, codepoints, warning: bool = False):
109
139
  )
110
140
  if count > 10:
111
141
  missing_str += f"\n and {count - 10} more."
112
- message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}"
142
+ message = f"Font {FontCache.get_name(file)} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}"
113
143
  if warning:
114
144
  _LOGGER.warning(message)
115
145
  else:
116
146
  raise cv.Invalid(message)
117
147
 
118
148
 
119
- def validate_glyphs(config):
149
+ def validate_font_config(config):
120
150
  """
121
151
  Check for duplicate codepoints, then check that all requested codepoints actually
122
152
  have glyphs defined in the appropriate font file.
@@ -143,8 +173,6 @@ def validate_glyphs(config):
143
173
  # Make setpoints and glyphspoints disjoint
144
174
  setpoints.difference_update(glyphspoints)
145
175
  if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
146
- # Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation
147
- # or a file format limitation
148
176
  if any(x >= 256 for x in setpoints.copy().union(glyphspoints)):
149
177
  raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255")
150
178
  else:
@@ -154,13 +182,14 @@ def validate_glyphs(config):
154
182
  points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
155
183
  glyphspoints.difference_update(points)
156
184
  setpoints.difference_update(points)
157
- check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points)
185
+ check_missing_glyphs(extra[CONF_FILE], points)
158
186
 
159
187
  # A named glyph that can't be provided is an error
160
- check_missing_glyphs(fileconf[CONF_PATH], glyphspoints)
188
+
189
+ check_missing_glyphs(fileconf, glyphspoints)
161
190
  # A missing glyph from a set is a warning.
162
191
  if not config[CONF_IGNORE_MISSING_GLYPHS]:
163
- check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True)
192
+ check_missing_glyphs(fileconf, setpoints, warning=True)
164
193
 
165
194
  # Populate the default after the above checks so that use of the default doesn't trigger errors
166
195
  if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]:
@@ -168,17 +197,32 @@ def validate_glyphs(config):
168
197
  config[CONF_GLYPHS] = [DEFAULT_GLYPHS]
169
198
  else:
170
199
  # set a default glyphset, intersected with what the font actually offers
171
- font = FONT_CACHE[fileconf[CONF_PATH]]
200
+ font = FONT_CACHE[fileconf]
172
201
  config[CONF_GLYPHS] = [
173
202
  chr(x)
174
203
  for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
175
204
  if font.get_char_index(x) != 0
176
205
  ]
177
206
 
207
+ if config[CONF_FILE][CONF_TYPE] == TYPE_LOCAL_BITMAP:
208
+ if CONF_SIZE in config:
209
+ raise cv.Invalid(
210
+ "Size is not a valid option for bitmap fonts, which are inherently fixed size"
211
+ )
212
+ elif CONF_SIZE not in config:
213
+ config[CONF_SIZE] = 20
214
+
178
215
  return config
179
216
 
180
217
 
181
218
  FONT_EXTENSIONS = (".ttf", ".woff", ".otf")
219
+ BITMAP_EXTENSIONS = (".bdf", ".pcf")
220
+
221
+
222
+ def validate_bitmap_file(value):
223
+ if not any(map(value.lower().endswith, BITMAP_EXTENSIONS)):
224
+ raise cv.Invalid(f"Only {', '.join(BITMAP_EXTENSIONS)} files are supported.")
225
+ return CORE.relative_config_path(cv.file_(value))
182
226
 
183
227
 
184
228
  def validate_truetype_file(value):
@@ -187,24 +231,40 @@ def validate_truetype_file(value):
187
231
  f"Please unzip the font archive '{value}' first and then use the .ttf files inside."
188
232
  )
189
233
  if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
190
- raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.")
234
+ raise cv.Invalid(f"Only {', '.join(FONT_EXTENSIONS)} files are supported.")
191
235
  return CORE.relative_config_path(cv.file_(value))
192
236
 
193
237
 
238
+ def add_local_file(value):
239
+ if value in FONT_CACHE:
240
+ return value
241
+ path = value[CONF_PATH]
242
+ if not os.path.isfile(path):
243
+ raise cv.Invalid(f"File '{path}' not found.")
244
+ FONT_CACHE[value] = path
245
+ return value
246
+
247
+
194
248
  TYPE_LOCAL = "local"
195
249
  TYPE_LOCAL_BITMAP = "local_bitmap"
196
250
  TYPE_GFONTS = "gfonts"
197
251
  TYPE_WEB = "web"
198
- LOCAL_SCHEMA = cv.Schema(
199
- {
200
- cv.Required(CONF_PATH): validate_truetype_file,
201
- }
252
+ LOCAL_SCHEMA = cv.All(
253
+ cv.Schema(
254
+ {
255
+ cv.Required(CONF_PATH): validate_truetype_file,
256
+ }
257
+ ),
258
+ add_local_file,
202
259
  )
203
260
 
204
- LOCAL_BITMAP_SCHEMA = cv.Schema(
205
- {
206
- cv.Required(CONF_PATH): cv.file_,
207
- }
261
+ LOCAL_BITMAP_SCHEMA = cv.All(
262
+ cv.Schema(
263
+ {
264
+ cv.Required(CONF_PATH): validate_bitmap_file,
265
+ }
266
+ ),
267
+ add_local_file,
208
268
  )
209
269
 
210
270
  FULLPATH_SCHEMA = cv.maybe_simple_value(
@@ -235,56 +295,59 @@ def _compute_local_font_path(value: dict) -> Path:
235
295
  h.update(url.encode())
236
296
  key = h.hexdigest()[:8]
237
297
  base_dir = external_files.compute_local_file_dir(DOMAIN)
238
- _LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key)
298
+ _LOGGER.debug("_compute_local_font_path: %s", base_dir / key)
239
299
  return base_dir / key
240
300
 
241
301
 
242
- def get_font_path(value, font_type) -> Path:
243
- if font_type == TYPE_GFONTS:
244
- name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
245
- return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
246
- if font_type == TYPE_WEB:
247
- return _compute_local_font_path(value) / "font.ttf"
248
- assert False
249
-
250
-
251
302
  def download_gfont(value):
303
+ if value in FONT_CACHE:
304
+ return value
252
305
  name = (
253
306
  f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
254
307
  )
255
308
  url = f"https://fonts.googleapis.com/css2?family={name}"
256
- path = get_font_path(value, TYPE_GFONTS)
257
- _LOGGER.debug("download_gfont: path=%s", path)
258
-
259
- try:
260
- req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
261
- req.raise_for_status()
262
- except requests.exceptions.RequestException as e:
263
- raise cv.Invalid(
264
- f"Could not download font at {url}, please check the fonts exists "
265
- f"at google fonts ({e})"
266
- )
267
- match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
268
- if match is None:
269
- raise cv.Invalid(
270
- f"Could not extract ttf file from gfonts response for {name}, "
271
- f"please report this."
272
- )
309
+ path = (
310
+ external_files.compute_local_file_dir(DOMAIN)
311
+ / f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1.ttf"
312
+ )
313
+ if not external_files.is_file_recent(str(path), value[CONF_REFRESH]):
314
+ _LOGGER.debug("download_gfont: path=%s", path)
315
+ try:
316
+ req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
317
+ req.raise_for_status()
318
+ except requests.exceptions.RequestException as e:
319
+ raise cv.Invalid(
320
+ f"Could not download font at {url}, please check the fonts exists "
321
+ f"at google fonts ({e})"
322
+ )
323
+ match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
324
+ if match is None:
325
+ raise cv.Invalid(
326
+ f"Could not extract ttf file from gfonts response for {name}, "
327
+ f"please report this."
328
+ )
273
329
 
274
- ttf_url = match.group(1)
275
- _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
330
+ ttf_url = match.group(1)
331
+ _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
276
332
 
277
- external_files.download_content(ttf_url, path)
278
- return FULLPATH_SCHEMA(path)
333
+ external_files.download_content(ttf_url, path)
334
+ # In case the remote file is not modified, the download_content function will return the existing file,
335
+ # so update the modification time to now.
336
+ path.touch()
337
+ FONT_CACHE[value] = path
338
+ return value
279
339
 
280
340
 
281
341
  def download_web_font(value):
342
+ if value in FONT_CACHE:
343
+ return value
282
344
  url = value[CONF_URL]
283
- path = get_font_path(value, TYPE_WEB)
345
+ path = _compute_local_font_path(value) / "font.ttf"
284
346
 
285
347
  external_files.download_content(url, path)
286
348
  _LOGGER.debug("download_web_font: path=%s", path)
287
- return FULLPATH_SCHEMA(path)
349
+ FONT_CACHE[value] = path
350
+ return value
288
351
 
289
352
 
290
353
  EXTERNAL_FONT_SCHEMA = cv.Schema(
@@ -340,14 +403,14 @@ def validate_file_shorthand(value):
340
403
  }
341
404
  )
342
405
 
343
- if value.endswith(".pcf") or value.endswith(".bdf"):
344
- value = convert_bitmap_to_pillow_font(
345
- CORE.relative_config_path(cv.file_(value))
406
+ extension = Path(value).suffix
407
+ if extension in BITMAP_EXTENSIONS:
408
+ return font_file_schema(
409
+ {
410
+ CONF_TYPE: TYPE_LOCAL_BITMAP,
411
+ CONF_PATH: value,
412
+ }
346
413
  )
347
- return {
348
- CONF_TYPE: TYPE_LOCAL_BITMAP,
349
- CONF_PATH: value,
350
- }
351
414
 
352
415
  return font_file_schema(
353
416
  {
@@ -391,7 +454,7 @@ FONT_SCHEMA = cv.Schema(
391
454
  cv.one_of(*glyphsets.defined_glyphsets())
392
455
  ),
393
456
  cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean,
394
- cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
457
+ cv.Optional(CONF_SIZE): cv.int_range(min=1),
395
458
  cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8),
396
459
  cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
397
460
  cv.Schema(
@@ -406,114 +469,19 @@ FONT_SCHEMA = cv.Schema(
406
469
  },
407
470
  )
408
471
 
409
- CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_glyphs)
410
-
411
-
412
- # PIL doesn't provide a consistent interface for both TrueType and bitmap
413
- # fonts. So, we use our own wrappers to give us the consistency that we need.
414
-
415
-
416
- class TrueTypeFontWrapper:
417
- def __init__(self, font):
418
- self.font = font
419
-
420
- def getoffset(self, glyph):
421
- _, (offset_x, offset_y) = self.font.font.getsize(glyph)
422
- return offset_x, offset_y
423
-
424
- def getmask(self, glyph, **kwargs):
425
- return self.font.getmask(str(glyph), **kwargs)
426
-
427
- def getmetrics(self, glyphs):
428
- return self.font.getmetrics()
429
-
430
-
431
- class BitmapFontWrapper:
432
- def __init__(self, font):
433
- self.font = font
434
- self.max_height = 0
435
-
436
- def getoffset(self, glyph):
437
- return 0, 0
438
-
439
- def getmask(self, glyph, **kwargs):
440
- return self.font.getmask(str(glyph), **kwargs)
441
-
442
- def getmetrics(self, glyphs):
443
- max_height = 0
444
- for glyph in glyphs:
445
- mask = self.getmask(glyph, mode="1")
446
- _, height = mask.size
447
- max_height = max(max_height, height)
448
- return max_height, 0
472
+ CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_font_config)
449
473
 
450
474
 
451
475
  class EFont:
452
- def __init__(self, file, size, codepoints):
476
+ def __init__(self, file, codepoints):
453
477
  self.codepoints = codepoints
454
- path = file[CONF_PATH]
455
- self.name = Path(path).name
456
- ftype = file[CONF_TYPE]
457
- if ftype == TYPE_LOCAL_BITMAP:
458
- self.font = load_bitmap_font(path)
459
- else:
460
- self.font = load_ttf_font(path, size)
461
- self.ascent, self.descent = self.font.getmetrics(codepoints)
462
-
463
-
464
- def convert_bitmap_to_pillow_font(filepath):
465
- from PIL import BdfFontFile, PcfFontFile
466
-
467
- local_bitmap_font_file = external_files.compute_local_file_dir(
468
- DOMAIN,
469
- ) / os.path.basename(filepath)
470
-
471
- copy_file_if_changed(filepath, local_bitmap_font_file)
472
-
473
- local_pil_font_file = local_bitmap_font_file.with_suffix(".pil")
474
- with open(local_bitmap_font_file, "rb") as fp:
475
- try:
476
- try:
477
- p = PcfFontFile.PcfFontFile(fp)
478
- except SyntaxError:
479
- fp.seek(0)
480
- p = BdfFontFile.BdfFontFile(fp)
481
-
482
- # Convert to pillow-formatted fonts, which have a .pil and .pbm extension.
483
- p.save(local_pil_font_file)
484
- except (SyntaxError, OSError) as err:
485
- raise core.EsphomeError(
486
- f"Failed to parse as bitmap font: '{filepath}': {err}"
487
- )
488
-
489
- return str(local_pil_font_file)
490
-
491
-
492
- def load_bitmap_font(filepath):
493
- from PIL import ImageFont
494
-
495
- try:
496
- font = ImageFont.load(str(filepath))
497
- except Exception as e:
498
- raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}")
499
-
500
- return BitmapFontWrapper(font)
501
-
502
-
503
- def load_ttf_font(path, size):
504
- from PIL import ImageFont
505
-
506
- try:
507
- font = ImageFont.truetype(str(path), size)
508
- except Exception as e:
509
- raise core.EsphomeError(f"Could not load TrueType file {path}: {e}")
510
-
511
- return TrueTypeFontWrapper(font)
478
+ self.font: Face = FONT_CACHE[file]
512
479
 
513
480
 
514
481
  class GlyphInfo:
515
- def __init__(self, data_len, offset_x, offset_y, width, height):
482
+ def __init__(self, data_len, advance, offset_x, offset_y, width, height):
516
483
  self.data_len = data_len
484
+ self.advance = advance
517
485
  self.offset_x = offset_x
518
486
  self.offset_y = offset_y
519
487
  self.width = width
@@ -537,15 +505,14 @@ async def to_code(config):
537
505
  }
538
506
  # get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets
539
507
  point_set.update(flatten(config[CONF_GLYPHS]))
540
- size = config[CONF_SIZE]
541
508
  # Create the codepoint to font file map
542
- base_font = EFont(config[CONF_FILE], size, point_set)
543
- point_font_map: dict[str, EFont] = {c: base_font for c in point_set}
509
+ base_font = FONT_CACHE[config[CONF_FILE]]
510
+ point_font_map: dict[str, Face] = {c: base_font for c in point_set}
544
511
  # process extras, updating the map and extending the codepoint list
545
512
  for extra in config[CONF_EXTRAS]:
546
513
  extra_points = flatten(extra[CONF_GLYPHS])
547
514
  point_set.update(extra_points)
548
- extra_font = EFont(extra[CONF_FILE], size, extra_points)
515
+ extra_font = FONT_CACHE[extra[CONF_FILE]]
549
516
  point_font_map.update({c: extra_font for c in extra_points})
550
517
 
551
518
  codepoints = list(point_set)
@@ -553,28 +520,52 @@ async def to_code(config):
553
520
  glyph_args = {}
554
521
  data = []
555
522
  bpp = config[CONF_BPP]
556
- if bpp == 1:
557
- mode = "1"
558
- scale = 1
559
- else:
560
- mode = "L"
561
- scale = 256 // (1 << bpp)
523
+ mode = ft_pixel_mode_grays
524
+ scale = 256 // (1 << bpp)
562
525
  # create the data array for all glyphs
563
526
  for codepoint in codepoints:
564
527
  font = point_font_map[codepoint]
565
- mask = font.font.getmask(codepoint, mode=mode)
566
- offset_x, offset_y = font.font.getoffset(codepoint)
567
- width, height = mask.size
528
+ if not font.has_fixed_sizes:
529
+ font.set_pixel_sizes(config[CONF_SIZE], 0)
530
+ font.load_char(codepoint)
531
+ font.glyph.render(mode)
532
+ width = font.glyph.bitmap.width
533
+ height = font.glyph.bitmap.rows
534
+ buffer = font.glyph.bitmap.buffer
535
+ pitch = font.glyph.bitmap.pitch
568
536
  glyph_data = [0] * ((height * width * bpp + 7) // 8)
537
+ src_mode = font.glyph.bitmap.pixel_mode
569
538
  pos = 0
570
539
  for y in range(height):
571
540
  for x in range(width):
572
- pixel = mask.getpixel((x, y)) // scale
541
+ if src_mode == ft_pixel_mode_mono:
542
+ pixel = (
543
+ (1 << bpp) - 1
544
+ if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
545
+ else 0
546
+ )
547
+ else:
548
+ pixel = buffer[y * pitch + x] // scale
573
549
  for bit_num in range(bpp):
574
550
  if pixel & (1 << (bpp - bit_num - 1)):
575
551
  glyph_data[pos // 8] |= 0x80 >> (pos % 8)
576
552
  pos += 1
577
- glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height)
553
+ ascender = font.size.ascender // 64
554
+ if ascender == 0:
555
+ if font.has_fixed_sizes:
556
+ ascender = font.available_sizes[0].height
557
+ else:
558
+ _LOGGER.error(
559
+ "Unable to determine ascender of font %s", config[CONF_FILE]
560
+ )
561
+ glyph_args[codepoint] = GlyphInfo(
562
+ len(data),
563
+ font.glyph.metrics.horiAdvance // 64,
564
+ font.glyph.bitmap_left,
565
+ ascender - font.glyph.bitmap_top,
566
+ width,
567
+ height,
568
+ )
578
569
  data += glyph_data
579
570
 
580
571
  rhs = [HexInt(x) for x in data]
@@ -598,6 +589,7 @@ async def to_code(config):
598
589
  f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
599
590
  ),
600
591
  ),
592
+ ("advance", glyph_args[codepoint].advance),
601
593
  ("offset_x", glyph_args[codepoint].offset_x),
602
594
  ("offset_y", glyph_args[codepoint].offset_y),
603
595
  ("width", glyph_args[codepoint].width),
@@ -607,11 +599,19 @@ async def to_code(config):
607
599
 
608
600
  glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
609
601
 
602
+ font_height = base_font.size.height // 64
603
+ ascender = base_font.size.ascender // 64
604
+ if font_height == 0:
605
+ if base_font.has_fixed_sizes:
606
+ font_height = base_font.available_sizes[0].height
607
+ ascender = font_height
608
+ else:
609
+ _LOGGER.error("Unable to determine height of font %s", config[CONF_FILE])
610
610
  cg.new_Pvariable(
611
611
  config[CONF_ID],
612
612
  glyphs,
613
613
  len(glyph_initializer),
614
- base_font.ascent,
615
- base_font.ascent + base_font.descent,
614
+ ascender,
615
+ font_height,
616
616
  bpp,
617
617
  )
@@ -81,7 +81,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
81
81
  if (glyph_n < 0) {
82
82
  // Unknown char, skip
83
83
  if (!this->get_glyphs().empty())
84
- x += this->get_glyphs()[0].glyph_data_->width;
84
+ x += this->get_glyphs()[0].glyph_data_->advance;
85
85
  i++;
86
86
  continue;
87
87
  }
@@ -92,7 +92,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
92
92
  } else {
93
93
  min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
94
94
  }
95
- x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
95
+ x += glyph.glyph_data_->advance;
96
96
 
97
97
  i += match_length;
98
98
  has_char = true;
@@ -111,7 +111,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
111
111
  // Unknown char, skip
112
112
  ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
113
113
  if (!this->get_glyphs().empty()) {
114
- uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width;
114
+ uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
115
115
  display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
116
116
  x_at += glyph_width;
117
117
  }
@@ -161,7 +161,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
161
161
  }
162
162
  }
163
163
  }
164
- x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
164
+ x_at += glyph.glyph_data_->advance;
165
165
 
166
166
  i += match_length;
167
167
  }
@@ -15,6 +15,7 @@ class Font;
15
15
  struct GlyphData {
16
16
  const uint8_t *a_char;
17
17
  const uint8_t *data;
18
+ int advance;
18
19
  int offset_x;
19
20
  int offset_y;
20
21
  int width;
@@ -1,9 +1,15 @@
1
- import logging
1
+ import logging
2
+
3
+ from esphome import automation
2
4
  import esphome.codegen as cg
5
+ from esphome.components import climate, logger, uart
6
+ from esphome.components.climate import (
7
+ CONF_CURRENT_TEMPERATURE,
8
+ ClimateMode,
9
+ ClimatePreset,
10
+ ClimateSwingMode,
11
+ )
3
12
  import esphome.config_validation as cv
4
- import esphome.final_validate as fv
5
- from esphome.components import uart, climate, logger
6
- from esphome import automation
7
13
  from esphome.const import (
8
14
  CONF_BEEPER,
9
15
  CONF_DISPLAY,
@@ -24,12 +30,7 @@ from esphome.const import (
24
30
  CONF_VISUAL,
25
31
  CONF_WIFI,
26
32
  )
27
- from esphome.components.climate import (
28
- ClimateMode,
29
- ClimatePreset,
30
- ClimateSwingMode,
31
- CONF_CURRENT_TEMPERATURE,
32
- )
33
+ import esphome.final_validate as fv
33
34
 
34
35
  _LOGGER = logging.getLogger(__name__)
35
36
 
@@ -12,7 +12,7 @@ float HBridgeSwitch::get_setup_priority() const { return setup_priority::HARDWAR
12
12
  void HBridgeSwitch::setup() {
13
13
  ESP_LOGCONFIG(TAG, "Setting up H-Bridge Switch '%s'...", this->name_.c_str());
14
14
 
15
- optional<bool> initial_state = this->get_initial_state_with_restore_mode().value_or(false);
15
+ optional<bool> initial_state = this->get_initial_state_with_restore_mode();
16
16
 
17
17
  // Like GPIOSwitch does, set the pin state both before and after pin setup()
18
18
  this->on_pin_->digital_write(false);
@@ -24,7 +24,7 @@ void HBridgeSwitch::setup() {
24
24
  this->off_pin_->digital_write(false);
25
25
 
26
26
  if (initial_state.has_value())
27
- this->write_state(initial_state);
27
+ this->write_state(initial_state.value());
28
28
  }
29
29
 
30
30
  void HBridgeSwitch::dump_config() {
@@ -53,6 +53,7 @@ PROTOCOLS = {
53
53
  "mitsubishi_sez": Protocol.PROTOCOL_MITSUBISHI_SEZ,
54
54
  "panasonic_ckp": Protocol.PROTOCOL_PANASONIC_CKP,
55
55
  "panasonic_dke": Protocol.PROTOCOL_PANASONIC_DKE,
56
+ "panasonic_eke": Protocol.PROTOCOL_PANASONIC_EKE,
56
57
  "panasonic_jke": Protocol.PROTOCOL_PANASONIC_JKE,
57
58
  "panasonic_lke": Protocol.PROTOCOL_PANASONIC_LKE,
58
59
  "panasonic_nke": Protocol.PROTOCOL_PANASONIC_NKE,
@@ -127,6 +128,6 @@ def to_code(config):
127
128
  cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
128
129
  cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
129
130
 
130
- cg.add_library("tonia/HeatpumpIR", "1.0.27")
131
+ cg.add_library("tonia/HeatpumpIR", "1.0.32")
131
132
  if CORE.is_libretiny:
132
133
  CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
@@ -47,6 +47,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP
47
47
  {PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
48
48
  {PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
49
49
  {PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
50
+ {PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
50
51
  {PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
51
52
  {PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
52
53
  {PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT