esphome 2024.10.3__py3-none-any.whl → 2024.11.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 (228) hide show
  1. esphome/__main__.py +22 -4
  2. esphome/automation.py +29 -2
  3. esphome/components/animation/__init__.py +5 -8
  4. esphome/components/animation/animation.cpp +1 -1
  5. esphome/components/audio/__init__.py +9 -0
  6. esphome/components/audio/audio.h +21 -0
  7. esphome/components/axs15231/__init__.py +6 -0
  8. esphome/components/axs15231/touchscreen/__init__.py +36 -0
  9. esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +64 -0
  10. esphome/components/axs15231/touchscreen/axs15231_touchscreen.h +27 -0
  11. esphome/components/bme68x_bsec2/__init__.py +1 -1
  12. esphome/components/bytebuffer/__init__.py +5 -0
  13. esphome/components/bytebuffer/bytebuffer.h +421 -0
  14. esphome/components/climate/__init__.py +14 -13
  15. esphome/components/datetime/__init__.py +3 -3
  16. esphome/components/debug/debug_esp32.cpp +16 -8
  17. esphome/components/dfplayer/dfplayer.cpp +132 -6
  18. esphome/components/dfplayer/dfplayer.h +19 -53
  19. esphome/components/display/display.cpp +142 -0
  20. esphome/components/display/display.h +7 -0
  21. esphome/components/es8311/__init__.py +0 -0
  22. esphome/components/es8311/audio_dac.py +70 -0
  23. esphome/components/es8311/es8311.cpp +227 -0
  24. esphome/components/es8311/es8311.h +135 -0
  25. esphome/components/es8311/es8311_const.h +195 -0
  26. esphome/components/esp32/boards.py +199 -1
  27. esphome/components/esp32/gpio.py +3 -1
  28. esphome/components/esp32_ble/const_esp32c6.h +7 -0
  29. esphome/components/esp32_ble_client/ble_client_base.h +1 -1
  30. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +2 -1
  31. esphome/components/esp32_rmt_led_strip/led_strip.cpp +2 -2
  32. esphome/components/esp32_rmt_led_strip/led_strip.h +2 -0
  33. esphome/components/esp32_rmt_led_strip/light.py +3 -1
  34. esphome/components/esp8266/gpio.py +7 -5
  35. esphome/components/ethernet/__init__.py +55 -1
  36. esphome/components/ethernet/ethernet_component.cpp +14 -1
  37. esphome/components/ethernet/ethernet_component.h +7 -1
  38. esphome/components/font/__init__.py +213 -108
  39. esphome/components/gp8403/output/__init__.py +1 -1
  40. esphome/components/host/gpio.py +6 -4
  41. esphome/components/http_request/__init__.py +12 -0
  42. esphome/components/http_request/http_request.h +65 -3
  43. esphome/components/http_request/http_request_arduino.cpp +2 -3
  44. esphome/components/http_request/http_request_idf.cpp +6 -14
  45. esphome/components/http_request/ota/ota_http_request.cpp +1 -1
  46. esphome/components/http_request/update/http_request_update.cpp +1 -1
  47. esphome/components/i2c_device/__init__.py +26 -0
  48. esphome/components/i2c_device/i2c_device.cpp +17 -0
  49. esphome/components/i2c_device/i2c_device.h +18 -0
  50. esphome/components/i2s_audio/__init__.py +1 -3
  51. esphome/components/i2s_audio/speaker/__init__.py +12 -4
  52. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +432 -197
  53. esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +91 -32
  54. esphome/components/ili9xxx/display.py +5 -1
  55. esphome/components/image/__init__.py +5 -8
  56. esphome/components/image/image.cpp +14 -14
  57. esphome/components/image/image.h +20 -24
  58. esphome/components/internal_temperature/internal_temperature.cpp +51 -2
  59. esphome/components/internal_temperature/internal_temperature.h +1 -0
  60. esphome/components/libretiny/gpio.py +4 -2
  61. esphome/components/light/__init__.py +32 -1
  62. esphome/components/light/automation.py +39 -32
  63. esphome/components/light/effects.py +36 -36
  64. esphome/components/light/light_state.cpp +6 -16
  65. esphome/components/light/light_state.h +34 -0
  66. esphome/components/light/types.py +3 -1
  67. esphome/components/logger/logger_esp32.cpp +15 -0
  68. esphome/components/lvgl/__init__.py +202 -95
  69. esphome/components/lvgl/automation.py +42 -40
  70. esphome/components/lvgl/binary_sensor/__init__.py +8 -15
  71. esphome/components/lvgl/defines.py +14 -8
  72. esphome/components/lvgl/encoders.py +11 -8
  73. esphome/components/lvgl/keypads.py +77 -0
  74. esphome/components/lvgl/light/__init__.py +6 -8
  75. esphome/components/lvgl/lv_validation.py +2 -4
  76. esphome/components/lvgl/lvcode.py +3 -9
  77. esphome/components/lvgl/lvgl_esphome.cpp +210 -89
  78. esphome/components/lvgl/lvgl_esphome.h +113 -30
  79. esphome/components/lvgl/lvgl_proxy.h +17 -0
  80. esphome/components/lvgl/number/__init__.py +10 -15
  81. esphome/components/lvgl/schemas.py +4 -2
  82. esphome/components/lvgl/select/__init__.py +12 -37
  83. esphome/components/lvgl/select/lvgl_select.h +27 -33
  84. esphome/components/lvgl/sensor/__init__.py +8 -14
  85. esphome/components/lvgl/styles.py +3 -4
  86. esphome/components/lvgl/switch/__init__.py +8 -13
  87. esphome/components/lvgl/text/__init__.py +5 -6
  88. esphome/components/lvgl/text_sensor/__init__.py +15 -15
  89. esphome/components/lvgl/touchscreens.py +2 -3
  90. esphome/components/lvgl/trigger.py +7 -9
  91. esphome/components/lvgl/types.py +9 -3
  92. esphome/components/lvgl/widgets/__init__.py +32 -21
  93. esphome/components/lvgl/widgets/dropdown.py +22 -10
  94. esphome/components/lvgl/widgets/msgbox.py +6 -5
  95. esphome/components/lvgl/widgets/obj.py +4 -2
  96. esphome/components/lvgl/widgets/page.py +3 -2
  97. esphome/components/lvgl/widgets/qrcode.py +54 -0
  98. esphome/components/lvgl/widgets/roller.py +21 -14
  99. esphome/components/lvgl/widgets/tileview.py +2 -1
  100. esphome/components/max17043/__init__.py +1 -0
  101. esphome/components/max17043/automation.h +20 -0
  102. esphome/components/max17043/max17043.cpp +98 -0
  103. esphome/components/max17043/max17043.h +29 -0
  104. esphome/components/max17043/sensor.py +77 -0
  105. esphome/components/media_player/__init__.py +11 -0
  106. esphome/components/media_player/automation.h +10 -0
  107. esphome/components/media_player/media_player.cpp +4 -0
  108. esphome/components/midea/air_conditioner.cpp +17 -1
  109. esphome/components/mlx90393/sensor.py +1 -1
  110. esphome/components/modbus_controller/__init__.py +31 -1
  111. esphome/components/modbus_controller/automation.h +16 -0
  112. esphome/components/modbus_controller/const.py +2 -0
  113. esphome/components/modbus_controller/modbus_controller.cpp +14 -2
  114. esphome/components/modbus_controller/modbus_controller.h +9 -0
  115. esphome/components/mopeka_pro_check/mopeka_pro_check.cpp +40 -21
  116. esphome/components/mopeka_pro_check/mopeka_pro_check.h +9 -2
  117. esphome/components/mopeka_pro_check/sensor.py +41 -0
  118. esphome/components/mqtt/__init__.py +36 -0
  119. esphome/components/mqtt/mqtt_client.cpp +27 -3
  120. esphome/components/mqtt/mqtt_client.h +27 -2
  121. esphome/components/mqtt/mqtt_climate.cpp +4 -2
  122. esphome/components/mqtt/mqtt_component.cpp +6 -0
  123. esphome/components/mqtt/mqtt_component.h +4 -0
  124. esphome/components/mqtt/mqtt_const.h +6 -0
  125. esphome/components/online_image/online_image.cpp +2 -8
  126. esphome/components/online_image/online_image.h +2 -6
  127. esphome/components/opentherm/__init__.py +35 -9
  128. esphome/components/opentherm/binary_sensor/__init__.py +33 -0
  129. esphome/components/opentherm/const.py +11 -0
  130. esphome/components/opentherm/generate.py +142 -0
  131. esphome/components/opentherm/hub.cpp +130 -24
  132. esphome/components/opentherm/hub.h +62 -9
  133. esphome/components/opentherm/input.h +18 -0
  134. esphome/components/opentherm/input.py +51 -0
  135. esphome/components/opentherm/number/__init__.py +74 -0
  136. esphome/components/opentherm/number/number.cpp +40 -0
  137. esphome/components/opentherm/number/number.h +31 -0
  138. esphome/components/opentherm/opentherm.cpp +30 -0
  139. esphome/components/opentherm/opentherm.h +34 -2
  140. esphome/components/opentherm/opentherm_macros.h +151 -0
  141. esphome/components/opentherm/output/__init__.py +47 -0
  142. esphome/components/opentherm/output/output.cpp +18 -0
  143. esphome/components/opentherm/output/output.h +33 -0
  144. esphome/components/opentherm/schema.py +814 -0
  145. esphome/components/opentherm/sensor/__init__.py +51 -0
  146. esphome/components/opentherm/switch/__init__.py +43 -0
  147. esphome/components/opentherm/switch/switch.cpp +28 -0
  148. esphome/components/opentherm/switch/switch.h +20 -0
  149. esphome/components/opentherm/validate.py +31 -0
  150. esphome/components/pcd8544/display.py +8 -4
  151. esphome/components/prometheus/prometheus_handler.cpp +176 -14
  152. esphome/components/prometheus/prometheus_handler.h +25 -7
  153. esphome/components/qspi_amoled/display.py +1 -141
  154. esphome/components/qspi_dbi/display.py +185 -0
  155. esphome/components/qspi_dbi/models.py +64 -0
  156. esphome/components/{qspi_amoled/qspi_amoled.cpp → qspi_dbi/qspi_dbi.cpp} +95 -46
  157. esphome/components/{qspi_amoled/qspi_amoled.h → qspi_dbi/qspi_dbi.h} +26 -15
  158. esphome/components/rp2040/__init__.py +6 -3
  159. esphome/components/rp2040/gpio.py +5 -3
  160. esphome/components/rtttl/rtttl.cpp +4 -1
  161. esphome/components/rtttl/rtttl.h +1 -0
  162. esphome/components/sdl/sdl_esphome.cpp +22 -5
  163. esphome/components/sdl/sdl_esphome.h +1 -0
  164. esphome/components/sensor/__init__.py +18 -8
  165. esphome/components/sensor/filter.cpp +19 -18
  166. esphome/components/sensor/filter.h +9 -10
  167. esphome/components/sgp4x/sgp4x.cpp +40 -74
  168. esphome/components/sgp4x/sgp4x.h +5 -3
  169. esphome/components/speaker/__init__.py +51 -5
  170. esphome/components/speaker/automation.h +25 -0
  171. esphome/components/speaker/speaker.h +72 -1
  172. esphome/components/spi/__init__.py +15 -14
  173. esphome/components/spi_device/__init__.py +4 -15
  174. esphome/components/ssd1306_spi/display.py +6 -2
  175. esphome/components/ssd1322_spi/display.py +6 -2
  176. esphome/components/ssd1325_spi/display.py +6 -2
  177. esphome/components/ssd1327_spi/display.py +6 -2
  178. esphome/components/ssd1331_spi/display.py +6 -2
  179. esphome/components/ssd1351_spi/display.py +6 -2
  180. esphome/components/st7567_spi/display.py +6 -2
  181. esphome/components/st7701s/display.py +5 -1
  182. esphome/components/st7735/display.py +10 -5
  183. esphome/components/st7789v/display.py +12 -7
  184. esphome/components/statsd/statsd.cpp +2 -0
  185. esphome/components/statsd/statsd.h +2 -0
  186. esphome/components/sun/sun.h +3 -0
  187. esphome/components/tc74/__init__.py +1 -0
  188. esphome/components/tc74/sensor.py +32 -0
  189. esphome/components/tc74/tc74.cpp +68 -0
  190. esphome/components/tc74/tc74.h +28 -0
  191. esphome/components/touchscreen/__init__.py +41 -50
  192. esphome/components/touchscreen/touchscreen.h +4 -8
  193. esphome/components/udp/udp_component.cpp +6 -3
  194. esphome/components/udp/udp_component.h +4 -2
  195. esphome/components/waveshare_epaper/display.py +6 -2
  196. esphome/components/web_server/web_server.cpp +22 -0
  197. esphome/components/web_server/web_server.h +3 -0
  198. esphome/components/weikai/weikai.h +2 -2
  199. esphome/components/wifi/wifi_component.cpp +2 -2
  200. esphome/components/wifi/wifi_component_esp32_arduino.cpp +4 -4
  201. esphome/components/wifi/wifi_component_esp8266.cpp +4 -4
  202. esphome/components/wifi/wifi_component_esp_idf.cpp +2 -2
  203. esphome/components/xpt2046/touchscreen/__init__.py +7 -32
  204. esphome/config_validation.py +3 -1
  205. esphome/const.py +8 -1
  206. esphome/core/defines.h +8 -2
  207. esphome/core/helpers.cpp +32 -17
  208. esphome/core/helpers.h +32 -16
  209. esphome/core/ring_buffer.cpp +2 -2
  210. esphome/core/ring_buffer.h +2 -2
  211. esphome/dashboard/core.py +25 -0
  212. esphome/dashboard/status/mdns.py +3 -4
  213. esphome/dashboard/web_server.py +54 -19
  214. esphome/espota2.py +36 -35
  215. esphome/helpers.py +68 -16
  216. esphome/mqtt.py +9 -2
  217. esphome/storage_json.py +4 -0
  218. esphome/writer.py +7 -18
  219. esphome/zeroconf.py +8 -6
  220. {esphome-2024.10.3.dist-info → esphome-2024.11.0b1.dist-info}/METADATA +7 -5
  221. {esphome-2024.10.3.dist-info → esphome-2024.11.0b1.dist-info}/RECORD +226 -180
  222. esphome/core/bytebuffer.cpp +0 -167
  223. esphome/core/bytebuffer.h +0 -144
  224. /esphome/components/{qspi_amoled → qspi_dbi}/__init__.py +0 -0
  225. {esphome-2024.10.3.dist-info → esphome-2024.11.0b1.dist-info}/LICENSE +0 -0
  226. {esphome-2024.10.3.dist-info → esphome-2024.11.0b1.dist-info}/WHEEL +0 -0
  227. {esphome-2024.10.3.dist-info → esphome-2024.11.0b1.dist-info}/entry_points.txt +0 -0
  228. {esphome-2024.10.3.dist-info → esphome-2024.11.0b1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
1
+ from collections.abc import Iterable
1
2
  import functools
2
3
  import hashlib
3
4
  import logging
@@ -5,6 +6,8 @@ import os
5
6
  from pathlib import Path
6
7
  import re
7
8
 
9
+ import freetype
10
+ import glyphsets
8
11
  from packaging import version
9
12
  import requests
10
13
 
@@ -43,6 +46,18 @@ GlyphData = font_ns.struct("GlyphData")
43
46
  CONF_BPP = "bpp"
44
47
  CONF_EXTRAS = "extras"
45
48
  CONF_FONTS = "fonts"
49
+ CONF_GLYPHSETS = "glyphsets"
50
+ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
51
+
52
+
53
+ # Cache loaded freetype fonts
54
+ class FontCache(dict):
55
+ def __missing__(self, key):
56
+ res = self[key] = freetype.Face(key)
57
+ return res
58
+
59
+
60
+ FONT_CACHE = FontCache()
46
61
 
47
62
 
48
63
  def glyph_comparator(x, y):
@@ -59,36 +74,106 @@ def glyph_comparator(x, y):
59
74
  return -1
60
75
  if len(x_) > len(y_):
61
76
  return 1
62
- raise cv.Invalid(f"Found duplicate glyph {x}")
63
-
64
-
65
- def validate_glyphs(value):
66
- if isinstance(value, list):
67
- value = cv.Schema([cv.string])(value)
68
- value = cv.Schema([cv.string])(list(value))
69
-
70
- value.sort(key=functools.cmp_to_key(glyph_comparator))
71
- return value
72
-
77
+ return 0
78
+
79
+
80
+ def flatten(lists) -> list:
81
+ """
82
+ Given a list of lists, flatten it to a single list of all elements of all lists.
83
+ This wraps itertools.chain.from_iterable to make it more readable, and return a list
84
+ rather than a single use iterable.
85
+ """
86
+ from itertools import chain
87
+
88
+ return list(chain.from_iterable(lists))
89
+
90
+
91
+ def check_missing_glyphs(file, codepoints: Iterable, warning: bool = False):
92
+ """
93
+ Check that the given font file actually contains the requested glyphs
94
+ :param file: A Truetype font file
95
+ :param codepoints: A list of codepoints to check
96
+ :param warning: If true, log a warning instead of raising an exception
97
+ """
98
+
99
+ font = FONT_CACHE[file]
100
+ missing = [chr(x) for x in codepoints if font.get_char_index(x) == 0]
101
+ if missing:
102
+ # Only list up to 10 missing glyphs
103
+ missing.sort(key=functools.cmp_to_key(glyph_comparator))
104
+ count = len(missing)
105
+ missing = missing[:10]
106
+ missing_str = "\n ".join(
107
+ f"{x} ({x.encode('unicode_escape')})" for x in missing
108
+ )
109
+ if count > 10:
110
+ missing_str += f"\n and {count - 10} more."
111
+ message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}"
112
+ if warning:
113
+ _LOGGER.warning(message)
114
+ else:
115
+ raise cv.Invalid(message)
73
116
 
74
- font_map = {}
75
117
 
118
+ def validate_glyphs(config):
119
+ """
120
+ Check for duplicate codepoints, then check that all requested codepoints actually
121
+ have glyphs defined in the appropriate font file.
122
+ """
76
123
 
77
- def merge_glyphs(config):
78
- glyphs = []
79
- glyphs.extend(config[CONF_GLYPHS])
80
- font_list = [(EFont(config[CONF_FILE], config[CONF_SIZE], config[CONF_GLYPHS]))]
81
- if extras := config.get(CONF_EXTRAS):
82
- extra_fonts = list(
83
- map(
84
- lambda x: EFont(x[CONF_FILE], config[CONF_SIZE], x[CONF_GLYPHS]), extras
85
- )
124
+ # Collect all glyph codepoints and flatten to a list of chars
125
+ glyphspoints = flatten(
126
+ [x[CONF_GLYPHS] for x in config[CONF_EXTRAS]] + config[CONF_GLYPHS]
127
+ )
128
+ # Convert a list of strings to a list of chars (one char strings)
129
+ glyphspoints = flatten([list(x) for x in glyphspoints])
130
+ if len(set(glyphspoints)) != len(glyphspoints):
131
+ duplicates = {x for x in glyphspoints if glyphspoints.count(x) > 1}
132
+ dup_str = ", ".join(f"{x} ({x.encode('unicode_escape')})" for x in duplicates)
133
+ raise cv.Invalid(
134
+ f"Found duplicate glyph{'s' if len(duplicates) != 1 else ''}: {dup_str}"
86
135
  )
87
- font_list.extend(extra_fonts)
88
- for extra in extras:
89
- glyphs.extend(extra[CONF_GLYPHS])
90
- validate_glyphs(glyphs)
91
- font_map[config[CONF_ID]] = font_list
136
+ # convert to codepoints
137
+ glyphspoints = {ord(x) for x in glyphspoints}
138
+ fileconf = config[CONF_FILE]
139
+ setpoints = set(
140
+ flatten([glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]])
141
+ )
142
+ # Make setpoints and glyphspoints disjoint
143
+ setpoints.difference_update(glyphspoints)
144
+ if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
145
+ # Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation
146
+ # or a file format limitation
147
+ if any(x >= 256 for x in setpoints.copy().union(glyphspoints)):
148
+ raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255")
149
+ else:
150
+ # for TT fonts, check that glyphs are actually present
151
+ # Check extras against their own font, exclude from parent font codepoints
152
+ for extra in config[CONF_EXTRAS]:
153
+ points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
154
+ glyphspoints.difference_update(points)
155
+ setpoints.difference_update(points)
156
+ check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points)
157
+
158
+ # A named glyph that can't be provided is an error
159
+ check_missing_glyphs(fileconf[CONF_PATH], glyphspoints)
160
+ # A missing glyph from a set is a warning.
161
+ if not config[CONF_IGNORE_MISSING_GLYPHS]:
162
+ check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True)
163
+
164
+ # Populate the default after the above checks so that use of the default doesn't trigger errors
165
+ if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]:
166
+ if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
167
+ config[CONF_GLYPHS] = [DEFAULT_GLYPHS]
168
+ else:
169
+ # set a default glyphset, intersected with what the font actually offers
170
+ font = FONT_CACHE[fileconf[CONF_PATH]]
171
+ config[CONF_GLYPHS] = [
172
+ chr(x)
173
+ for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
174
+ if font.get_char_index(x) != 0
175
+ ]
176
+
92
177
  return config
93
178
 
94
179
 
@@ -98,13 +183,13 @@ def validate_pillow_installed(value):
98
183
  except ImportError as err:
99
184
  raise cv.Invalid(
100
185
  "Please install the pillow python package to use this feature. "
101
- '(pip install "pillow==10.2.0")'
186
+ '(pip install "pillow==10.4.0")'
102
187
  ) from err
103
188
 
104
- if version.parse(PIL.__version__) != version.parse("10.2.0"):
189
+ if version.parse(PIL.__version__) != version.parse("10.4.0"):
105
190
  raise cv.Invalid(
106
- "Please update your pillow installation to 10.2.0. "
107
- '(pip install "pillow==10.2.0")'
191
+ "Please update your pillow installation to 10.4.0. "
192
+ '(pip install "pillow==10.4.0")'
108
193
  )
109
194
 
110
195
  return value
@@ -120,7 +205,7 @@ def validate_truetype_file(value):
120
205
  )
121
206
  if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
122
207
  raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.")
123
- return cv.file_(value)
208
+ return CORE.relative_config_path(cv.file_(value))
124
209
 
125
210
 
126
211
  TYPE_LOCAL = "local"
@@ -139,6 +224,10 @@ LOCAL_BITMAP_SCHEMA = cv.Schema(
139
224
  }
140
225
  )
141
226
 
227
+ FULLPATH_SCHEMA = cv.maybe_simple_value(
228
+ {cv.Required(CONF_PATH): cv.string}, key=CONF_PATH
229
+ )
230
+
142
231
  CONF_ITALIC = "italic"
143
232
  FONT_WEIGHTS = {
144
233
  "thin": 100,
@@ -167,13 +256,13 @@ def _compute_local_font_path(value: dict) -> Path:
167
256
  return base_dir / key
168
257
 
169
258
 
170
- def get_font_path(value, type) -> Path:
171
- if type == TYPE_GFONTS:
259
+ def get_font_path(value, font_type) -> Path:
260
+ if font_type == TYPE_GFONTS:
172
261
  name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
173
262
  return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
174
- if type == TYPE_WEB:
263
+ if font_type == TYPE_WEB:
175
264
  return _compute_local_font_path(value) / "font.ttf"
176
- return None
265
+ assert False
177
266
 
178
267
 
179
268
  def download_gfont(value):
@@ -203,7 +292,7 @@ def download_gfont(value):
203
292
  _LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
204
293
 
205
294
  external_files.download_content(ttf_url, path)
206
- return value
295
+ return FULLPATH_SCHEMA(path)
207
296
 
208
297
 
209
298
  def download_web_font(value):
@@ -212,7 +301,7 @@ def download_web_font(value):
212
301
 
213
302
  external_files.download_content(url, path)
214
303
  _LOGGER.debug("download_web_font: path=%s", path)
215
- return value
304
+ return FULLPATH_SCHEMA(path)
216
305
 
217
306
 
218
307
  EXTERNAL_FONT_SCHEMA = cv.Schema(
@@ -225,7 +314,6 @@ EXTERNAL_FONT_SCHEMA = cv.Schema(
225
314
  }
226
315
  )
227
316
 
228
-
229
317
  GFONTS_SCHEMA = cv.All(
230
318
  EXTERNAL_FONT_SCHEMA.extend(
231
319
  {
@@ -259,10 +347,10 @@ def validate_file_shorthand(value):
259
347
  }
260
348
  if weight is not None:
261
349
  data[CONF_WEIGHT] = weight[1:]
262
- return FILE_SCHEMA(data)
350
+ return font_file_schema(data)
263
351
 
264
352
  if value.startswith("http://") or value.startswith("https://"):
265
- return FILE_SCHEMA(
353
+ return font_file_schema(
266
354
  {
267
355
  CONF_TYPE: TYPE_WEB,
268
356
  CONF_URL: value,
@@ -270,14 +358,15 @@ def validate_file_shorthand(value):
270
358
  )
271
359
 
272
360
  if value.endswith(".pcf") or value.endswith(".bdf"):
273
- return FILE_SCHEMA(
274
- {
275
- CONF_TYPE: TYPE_LOCAL_BITMAP,
276
- CONF_PATH: value,
277
- }
361
+ value = convert_bitmap_to_pillow_font(
362
+ CORE.relative_config_path(cv.file_(value))
278
363
  )
364
+ return {
365
+ CONF_TYPE: TYPE_LOCAL_BITMAP,
366
+ CONF_PATH: value,
367
+ }
279
368
 
280
- return FILE_SCHEMA(
369
+ return font_file_schema(
281
370
  {
282
371
  CONF_TYPE: TYPE_LOCAL,
283
372
  CONF_PATH: value,
@@ -295,31 +384,35 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
295
384
  )
296
385
 
297
386
 
298
- def _file_schema(value):
387
+ def font_file_schema(value):
299
388
  if isinstance(value, str):
300
389
  return validate_file_shorthand(value)
301
390
  return TYPED_FILE_SCHEMA(value)
302
391
 
303
392
 
304
- FILE_SCHEMA = cv.All(_file_schema)
393
+ # Default if no glyphs or glyphsets are provided
394
+ DEFAULT_GLYPHSET = "GF_Latin_Kernel"
395
+ # default for bitmap fonts
396
+ DEFAULT_GLYPHS = ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz<C2><B0>'
305
397
 
306
- DEFAULT_GLYPHS = (
307
- ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
308
- )
309
398
  CONF_RAW_GLYPH_ID = "raw_glyph_id"
310
399
 
311
400
  FONT_SCHEMA = cv.Schema(
312
401
  {
313
402
  cv.Required(CONF_ID): cv.declare_id(Font),
314
- cv.Required(CONF_FILE): FILE_SCHEMA,
315
- cv.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs,
403
+ cv.Required(CONF_FILE): font_file_schema,
404
+ cv.Optional(CONF_GLYPHS, default=[]): cv.ensure_list(cv.string_strict),
405
+ cv.Optional(CONF_GLYPHSETS, default=[]): cv.ensure_list(
406
+ cv.one_of(*glyphsets.defined_glyphsets())
407
+ ),
408
+ cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean,
316
409
  cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
317
410
  cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8),
318
- cv.Optional(CONF_EXTRAS): cv.ensure_list(
411
+ cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
319
412
  cv.Schema(
320
413
  {
321
- cv.Required(CONF_FILE): FILE_SCHEMA,
322
- cv.Required(CONF_GLYPHS): validate_glyphs,
414
+ cv.Required(CONF_FILE): font_file_schema,
415
+ cv.Required(CONF_GLYPHS): cv.ensure_list(cv.string_strict),
323
416
  }
324
417
  )
325
418
  ),
@@ -328,7 +421,7 @@ FONT_SCHEMA = cv.Schema(
328
421
  },
329
422
  )
330
423
 
331
- CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, merge_glyphs)
424
+ CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA, validate_glyphs)
332
425
 
333
426
 
334
427
  # PIL doesn't provide a consistent interface for both TrueType and bitmap
@@ -344,7 +437,7 @@ class TrueTypeFontWrapper:
344
437
  return offset_x, offset_y
345
438
 
346
439
  def getmask(self, glyph, **kwargs):
347
- return self.font.getmask(glyph, **kwargs)
440
+ return self.font.getmask(str(glyph), **kwargs)
348
441
 
349
442
  def getmetrics(self, glyphs):
350
443
  return self.font.getmetrics()
@@ -359,7 +452,7 @@ class BitmapFontWrapper:
359
452
  return 0, 0
360
453
 
361
454
  def getmask(self, glyph, **kwargs):
362
- return self.font.getmask(glyph, **kwargs)
455
+ return self.font.getmask(str(glyph), **kwargs)
363
456
 
364
457
  def getmetrics(self, glyphs):
365
458
  max_height = 0
@@ -367,28 +460,20 @@ class BitmapFontWrapper:
367
460
  mask = self.getmask(glyph, mode="1")
368
461
  _, height = mask.size
369
462
  max_height = max(max_height, height)
370
- return (max_height, 0)
463
+ return max_height, 0
371
464
 
372
465
 
373
466
  class EFont:
374
- def __init__(self, file, size, glyphs):
375
- self.glyphs = glyphs
467
+ def __init__(self, file, size, codepoints):
468
+ self.codepoints = codepoints
469
+ path = file[CONF_PATH]
470
+ self.name = Path(path).name
376
471
  ftype = file[CONF_TYPE]
377
472
  if ftype == TYPE_LOCAL_BITMAP:
378
- font = load_bitmap_font(CORE.relative_config_path(file[CONF_PATH]))
379
- elif ftype == TYPE_LOCAL:
380
- path = CORE.relative_config_path(file[CONF_PATH])
381
- font = load_ttf_font(path, size)
382
- elif ftype in (TYPE_GFONTS, TYPE_WEB):
383
- path = get_font_path(file, ftype)
384
- font = load_ttf_font(path, size)
473
+ self.font = load_bitmap_font(path)
385
474
  else:
386
- raise cv.Invalid(f"Could not load font: unknown type: {ftype}")
387
- self.font = font
388
- self.ascent, self.descent = font.getmetrics(glyphs)
389
-
390
- def has_glyph(self, glyph):
391
- return glyph in self.glyphs
475
+ self.font = load_ttf_font(path, size)
476
+ self.ascent, self.descent = self.font.getmetrics(codepoints)
392
477
 
393
478
 
394
479
  def convert_bitmap_to_pillow_font(filepath):
@@ -400,6 +485,7 @@ def convert_bitmap_to_pillow_font(filepath):
400
485
 
401
486
  copy_file_if_changed(filepath, local_bitmap_font_file)
402
487
 
488
+ local_pil_font_file = local_bitmap_font_file.with_suffix(".pil")
403
489
  with open(local_bitmap_font_file, "rb") as fp:
404
490
  try:
405
491
  try:
@@ -409,28 +495,22 @@ def convert_bitmap_to_pillow_font(filepath):
409
495
  p = BdfFontFile.BdfFontFile(fp)
410
496
 
411
497
  # Convert to pillow-formatted fonts, which have a .pil and .pbm extension.
412
- p.save(local_bitmap_font_file)
498
+ p.save(local_pil_font_file)
413
499
  except (SyntaxError, OSError) as err:
414
500
  raise core.EsphomeError(
415
501
  f"Failed to parse as bitmap font: '{filepath}': {err}"
416
502
  )
417
503
 
418
- local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil"
419
- return cv.file_(local_pil_font_file)
504
+ return str(local_pil_font_file)
420
505
 
421
506
 
422
507
  def load_bitmap_font(filepath):
423
508
  from PIL import ImageFont
424
509
 
425
- # Convert bpf and pcf files to pillow fonts, first.
426
- pil_font_path = convert_bitmap_to_pillow_font(filepath)
427
-
428
510
  try:
429
- font = ImageFont.load(str(pil_font_path))
511
+ font = ImageFont.load(str(filepath))
430
512
  except Exception as e:
431
- raise core.EsphomeError(
432
- f"Failed to load bitmap font file: {pil_font_path} : {e}"
433
- )
513
+ raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}")
434
514
 
435
515
  return BitmapFontWrapper(font)
436
516
 
@@ -441,7 +521,7 @@ def load_ttf_font(path, size):
441
521
  try:
442
522
  font = ImageFont.truetype(str(path), size)
443
523
  except Exception as e:
444
- raise core.EsphomeError(f"Could not load truetype file {path}: {e}")
524
+ raise core.EsphomeError(f"Could not load TrueType file {path}: {e}")
445
525
 
446
526
  return TrueTypeFontWrapper(font)
447
527
 
@@ -456,14 +536,35 @@ class GlyphInfo:
456
536
 
457
537
 
458
538
  async def to_code(config):
459
- glyph_to_font_map = {}
460
- font_list = font_map[config[CONF_ID]]
461
- glyphs = []
462
- for font in font_list:
463
- glyphs.extend(font.glyphs)
464
- for glyph in font.glyphs:
465
- glyph_to_font_map[glyph] = font
466
- glyphs.sort(key=functools.cmp_to_key(glyph_comparator))
539
+ """
540
+ Collect all glyph codepoints, construct a map from a codepoint to a font file.
541
+ Codepoints are either explicit (glyphs key in top level or extras) or part of a glyphset.
542
+ Codepoints listed in extras use the extra font and override codepoints from glyphsets.
543
+ Achieve this by processing the base codepoints first, then the extras
544
+ """
545
+
546
+ # get the codepoints from glyphsets and flatten to a set of chrs.
547
+ point_set: set[str] = {
548
+ chr(x)
549
+ for x in flatten(
550
+ [glyphsets.unicodes_per_glyphset(x) for x in config[CONF_GLYPHSETS]]
551
+ )
552
+ }
553
+ # get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets
554
+ point_set.update(flatten(config[CONF_GLYPHS]))
555
+ size = config[CONF_SIZE]
556
+ # Create the codepoint to font file map
557
+ base_font = EFont(config[CONF_FILE], size, point_set)
558
+ point_font_map: dict[str, EFont] = {c: base_font for c in point_set}
559
+ # process extras, updating the map and extending the codepoint list
560
+ for extra in config[CONF_EXTRAS]:
561
+ extra_points = flatten(extra[CONF_GLYPHS])
562
+ point_set.update(extra_points)
563
+ extra_font = EFont(extra[CONF_FILE], size, extra_points)
564
+ point_font_map.update({c: extra_font for c in extra_points})
565
+
566
+ codepoints = list(point_set)
567
+ codepoints.sort(key=functools.cmp_to_key(glyph_comparator))
467
568
  glyph_args = {}
468
569
  data = []
469
570
  bpp = config[CONF_BPP]
@@ -473,10 +574,11 @@ async def to_code(config):
473
574
  else:
474
575
  mode = "L"
475
576
  scale = 256 // (1 << bpp)
476
- for glyph in glyphs:
477
- font = glyph_to_font_map[glyph].font
478
- mask = font.getmask(glyph, mode=mode)
479
- offset_x, offset_y = font.getoffset(glyph)
577
+ # create the data array for all glyphs
578
+ for codepoint in codepoints:
579
+ font = point_font_map[codepoint]
580
+ mask = font.font.getmask(codepoint, mode=mode)
581
+ offset_x, offset_y = font.font.getoffset(codepoint)
480
582
  width, height = mask.size
481
583
  glyph_data = [0] * ((height * width * bpp + 7) // 8)
482
584
  pos = 0
@@ -487,31 +589,34 @@ async def to_code(config):
487
589
  if pixel & (1 << (bpp - bit_num - 1)):
488
590
  glyph_data[pos // 8] |= 0x80 >> (pos % 8)
489
591
  pos += 1
490
- glyph_args[glyph] = GlyphInfo(len(data), offset_x, offset_y, width, height)
592
+ glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height)
491
593
  data += glyph_data
492
594
 
493
595
  rhs = [HexInt(x) for x in data]
494
596
  prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
495
597
 
598
+ # Create the glyph table that points to data in the above array.
496
599
  glyph_initializer = []
497
- for glyph in glyphs:
600
+ for codepoint in codepoints:
498
601
  glyph_initializer.append(
499
602
  cg.StructInitializer(
500
603
  GlyphData,
501
604
  (
502
605
  "a_char",
503
- cg.RawExpression(f"(const uint8_t *){cpp_string_escape(glyph)}"),
606
+ cg.RawExpression(
607
+ f"(const uint8_t *){cpp_string_escape(codepoint)}"
608
+ ),
504
609
  ),
505
610
  (
506
611
  "data",
507
612
  cg.RawExpression(
508
- f"{str(prog_arr)} + {str(glyph_args[glyph].data_len)}"
613
+ f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
509
614
  ),
510
615
  ),
511
- ("offset_x", glyph_args[glyph].offset_x),
512
- ("offset_y", glyph_args[glyph].offset_y),
513
- ("width", glyph_args[glyph].width),
514
- ("height", glyph_args[glyph].height),
616
+ ("offset_x", glyph_args[codepoint].offset_x),
617
+ ("offset_y", glyph_args[codepoint].offset_y),
618
+ ("width", glyph_args[codepoint].width),
619
+ ("height", glyph_args[codepoint].height),
515
620
  )
516
621
  )
517
622
 
@@ -521,7 +626,7 @@ async def to_code(config):
521
626
  config[CONF_ID],
522
627
  glyphs,
523
628
  len(glyph_initializer),
524
- font_list[0].ascent,
525
- font_list[0].ascent + font_list[0].descent,
629
+ base_font.ascent,
630
+ base_font.ascent + base_font.descent,
526
631
  bpp,
527
632
  )
@@ -16,7 +16,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
16
16
  {
17
17
  cv.GenerateID(): cv.declare_id(GP8403Output),
18
18
  cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403),
19
- cv.Required(CONF_CHANNEL): cv.one_of(0, 1),
19
+ cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=1),
20
20
  }
21
21
  ).extend(cv.COMPONENT_SCHEMA)
22
22
 
@@ -1,5 +1,8 @@
1
1
  import logging
2
2
 
3
+ from esphome import pins
4
+ import esphome.codegen as cg
5
+ import esphome.config_validation as cv
3
6
  from esphome.const import (
4
7
  CONF_ID,
5
8
  CONF_INPUT,
@@ -11,9 +14,6 @@ from esphome.const import (
11
14
  CONF_PULLDOWN,
12
15
  CONF_PULLUP,
13
16
  )
14
- from esphome import pins
15
- import esphome.config_validation as cv
16
- import esphome.codegen as cg
17
17
 
18
18
  from .const import host_ns
19
19
 
@@ -28,8 +28,10 @@ def _translate_pin(value):
28
28
  "This variable only supports pin numbers, not full pin schemas "
29
29
  "(with inverted and mode)."
30
30
  )
31
- if isinstance(value, int):
31
+ if isinstance(value, int) and not isinstance(value, bool):
32
32
  return value
33
+ if not isinstance(value, str):
34
+ raise cv.Invalid(f"Invalid pin number: {value}")
33
35
  try:
34
36
  return int(value)
35
37
  except ValueError:
@@ -6,6 +6,7 @@ from esphome.const import (
6
6
  CONF_ESP8266_DISABLE_SSL_SUPPORT,
7
7
  CONF_ID,
8
8
  CONF_METHOD,
9
+ CONF_ON_ERROR,
9
10
  CONF_TIMEOUT,
10
11
  CONF_TRIGGER_ID,
11
12
  CONF_URL,
@@ -185,6 +186,13 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
185
186
  cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
186
187
  {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)}
187
188
  ),
189
+ cv.Optional(CONF_ON_ERROR): automation.validate_automation(
190
+ {
191
+ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
192
+ automation.Trigger.template()
193
+ )
194
+ }
195
+ ),
188
196
  cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
189
197
  }
190
198
  )
@@ -272,5 +280,9 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
272
280
  ],
273
281
  conf,
274
282
  )
283
+ for conf in config.get(CONF_ON_ERROR, []):
284
+ trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
285
+ cg.add(var.register_error_trigger(trigger))
286
+ await automation.build_automation(trigger, [], conf)
275
287
 
276
288
  return var