esphome 2025.2.2__py3-none-any.whl → 2025.3.0__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.
- esphome/__main__.py +9 -1
- esphome/components/api/api_connection.cpp +426 -70
- esphome/components/api/api_connection.h +117 -25
- esphome/components/api/api_pb2.cpp +33 -0
- esphome/components/api/api_pb2.h +4 -0
- esphome/components/api/api_server.cpp +2 -2
- esphome/components/api/list_entities.cpp +76 -22
- esphome/components/api/list_entities.h +1 -0
- esphome/components/api/subscribe_state.h +2 -0
- esphome/components/audio/__init__.py +1 -1
- esphome/components/audio/audio_decoder.cpp +43 -11
- esphome/components/audio/audio_reader.cpp +2 -2
- esphome/components/audio/audio_resampler.cpp +4 -2
- esphome/components/audio/audio_transfer_buffer.cpp +19 -9
- esphome/components/audio/audio_transfer_buffer.h +7 -2
- esphome/components/bluetooth_proxy/bluetooth_proxy.h +8 -0
- esphome/components/bmp085/bmp085.cpp +1 -1
- esphome/components/chsc6x/__init__.py +2 -0
- esphome/components/chsc6x/chsc6x_touchscreen.cpp +47 -0
- esphome/components/chsc6x/chsc6x_touchscreen.h +34 -0
- esphome/components/chsc6x/touchscreen.py +33 -0
- esphome/components/climate/__init__.py +0 -1
- esphome/components/cst816/binary_sensor/__init__.py +2 -25
- esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +3 -14
- esphome/components/cst816/touchscreen/cst816_touchscreen.h +0 -4
- esphome/components/esp32_ble_beacon/__init__.py +3 -1
- esphome/components/esp8266/gpio.py +1 -2
- esphome/components/font/__init__.py +198 -215
- esphome/components/font/font.cpp +4 -4
- esphome/components/font/font.h +1 -0
- esphome/components/graph/graph.cpp +4 -0
- esphome/components/graph/graph.h +4 -0
- esphome/components/haier/climate.py +11 -10
- esphome/components/hbridge/switch/hbridge_switch.cpp +2 -2
- esphome/components/heatpumpir/climate.py +2 -1
- esphome/components/heatpumpir/heatpumpir.cpp +1 -0
- esphome/components/heatpumpir/heatpumpir.h +1 -0
- esphome/components/i2c/__init__.py +6 -6
- esphome/components/i2c/i2c_bus_esp_idf.cpp +6 -2
- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
- esphome/components/ili9xxx/display.py +1 -0
- esphome/components/ili9xxx/ili9xxx_display.h +5 -0
- esphome/components/ili9xxx/ili9xxx_init.h +59 -0
- esphome/components/ld2450/__init__.py +51 -0
- esphome/components/ld2450/binary_sensor.py +47 -0
- esphome/components/ld2450/button/__init__.py +45 -0
- esphome/components/ld2450/button/reset_button.cpp +9 -0
- esphome/components/ld2450/button/reset_button.h +18 -0
- esphome/components/ld2450/button/restart_button.cpp +9 -0
- esphome/components/ld2450/button/restart_button.h +18 -0
- esphome/components/ld2450/ld2450.cpp +876 -0
- esphome/components/ld2450/ld2450.h +234 -0
- esphome/components/ld2450/number/__init__.py +121 -0
- esphome/components/ld2450/number/presence_timeout_number.cpp +12 -0
- esphome/components/ld2450/number/presence_timeout_number.h +18 -0
- esphome/components/ld2450/number/zone_coordinate_number.cpp +14 -0
- esphome/components/ld2450/number/zone_coordinate_number.h +19 -0
- esphome/components/ld2450/select/__init__.py +56 -0
- esphome/components/ld2450/select/baud_rate_select.cpp +12 -0
- esphome/components/ld2450/select/baud_rate_select.h +18 -0
- esphome/components/ld2450/select/zone_type_select.cpp +12 -0
- esphome/components/ld2450/select/zone_type_select.h +18 -0
- esphome/components/ld2450/sensor.py +156 -0
- esphome/components/ld2450/switch/__init__.py +45 -0
- esphome/components/ld2450/switch/bluetooth_switch.cpp +12 -0
- esphome/components/ld2450/switch/bluetooth_switch.h +18 -0
- esphome/components/ld2450/switch/multi_target_switch.cpp +12 -0
- esphome/components/ld2450/switch/multi_target_switch.h +18 -0
- esphome/components/ld2450/text_sensor.py +62 -0
- esphome/components/lvgl/defines.py +0 -2
- esphome/components/lvgl/font.cpp +1 -1
- esphome/components/lvgl/lvgl_esphome.cpp +27 -19
- esphome/components/lvgl/widgets/img.py +1 -3
- esphome/components/mcp2515/mcp2515.cpp +1 -0
- esphome/components/mdns/__init__.py +1 -1
- esphome/components/mixer/speaker/mixer_speaker.cpp +6 -1
- esphome/components/mixer/speaker/mixer_speaker.h +2 -0
- esphome/components/mlx90393/sensor.py +53 -33
- esphome/components/mlx90393/sensor_mlx90393.cpp +4 -0
- esphome/components/mlx90393/sensor_mlx90393.h +8 -3
- esphome/components/mqtt/__init__.py +2 -2
- esphome/components/msa3xx/__init__.py +189 -0
- esphome/components/msa3xx/binary_sensor.py +40 -0
- esphome/components/msa3xx/msa3xx.cpp +417 -0
- esphome/components/msa3xx/msa3xx.h +311 -0
- esphome/components/msa3xx/sensor.py +42 -0
- esphome/components/msa3xx/text_sensor.py +38 -0
- esphome/components/nfc/binary_sensor/__init__.py +4 -4
- esphome/components/opentherm/binary_sensor/__init__.py +4 -4
- esphome/components/opentherm/generate.py +6 -6
- esphome/components/opentherm/sensor/__init__.py +5 -6
- esphome/components/packages/__init__.py +35 -11
- esphome/components/pn532/binary_sensor.py +4 -4
- esphome/components/rc522/binary_sensor.py +4 -4
- esphome/components/resampler/speaker/resampler_speaker.h +2 -0
- esphome/components/socket/bsd_sockets_impl.cpp +1 -0
- esphome/components/socket/lwip_sockets_impl.cpp +1 -0
- esphome/components/socket/socket.h +3 -1
- esphome/components/speaker/speaker.h +2 -2
- esphome/components/ssd1306_base/__init__.py +7 -7
- esphome/components/thermostat/climate.py +1 -1
- esphome/components/tmp1075/tmp1075.cpp +7 -11
- esphome/components/tmp1075/tmp1075.h +1 -2
- esphome/components/tormatic/__init__.py +1 -0
- esphome/components/tormatic/cover.py +47 -0
- esphome/components/tormatic/tormatic_cover.cpp +355 -0
- esphome/components/tormatic/tormatic_cover.h +60 -0
- esphome/components/tormatic/tormatic_protocol.h +211 -0
- esphome/components/touchscreen/binary_sensor/__init__.py +3 -0
- esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +7 -1
- esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +3 -1
- esphome/components/touchscreen/touchscreen.cpp +3 -4
- esphome/components/udp/udp_component.h +4 -1
- esphome/components/web_server/list_entities.cpp +70 -66
- esphome/components/web_server/list_entities.h +43 -22
- esphome/components/web_server/web_server.cpp +345 -68
- esphome/components/web_server/web_server.h +138 -6
- esphome/components/web_server_base/__init__.py +1 -1
- esphome/components/web_server_idf/__init__.py +2 -0
- esphome/components/web_server_idf/web_server_idf.cpp +177 -30
- esphome/components/web_server_idf/web_server_idf.h +53 -4
- esphome/config_validation.py +23 -125
- esphome/const.py +5 -1
- esphome/core/config.py +15 -6
- esphome/core/defines.h +1 -1
- esphome/core/helpers.h +24 -3
- esphome/core/time.cpp +1 -0
- esphome/cpp_generator.py +3 -3
- esphome/dashboard/core.py +30 -21
- esphome/dashboard/dns.py +7 -1
- esphome/dashboard/entries.py +83 -16
- esphome/dashboard/settings.py +0 -4
- esphome/dashboard/status/mdns.py +43 -14
- esphome/dashboard/status/mqtt.py +22 -9
- esphome/dashboard/status/ping.py +54 -10
- esphome/dashboard/web_server.py +56 -24
- esphome/storage_json.py +4 -0
- esphome/wizard.py +13 -17
- esphome/writer.py +1 -3
- esphome/yaml_util.py +36 -33
- esphome/zeroconf.py +9 -21
- {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/METADATA +7 -7
- {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/RECORD +147 -107
- esphome/components/cst816/binary_sensor/cst816_button.h +0 -27
- {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/LICENSE +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/WHEEL +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/entry_points.txt +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0.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
|
10
|
+
from freetype import Face, ft_pixel_mode_grays, ft_pixel_mode_mono
|
10
11
|
import requests
|
11
12
|
|
12
|
-
from esphome import
|
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
|
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(
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
return
|
57
|
-
|
58
|
-
|
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,21 @@ 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 {
|
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
|
149
|
+
def pt_to_px(pt):
|
150
|
+
"""
|
151
|
+
Convert a point size to pixels, rounding up to the nearest pixel
|
152
|
+
"""
|
153
|
+
return (pt + 63) // 64
|
154
|
+
|
155
|
+
|
156
|
+
def validate_font_config(config):
|
120
157
|
"""
|
121
158
|
Check for duplicate codepoints, then check that all requested codepoints actually
|
122
159
|
have glyphs defined in the appropriate font file.
|
@@ -142,43 +179,51 @@ def validate_glyphs(config):
|
|
142
179
|
)
|
143
180
|
# Make setpoints and glyphspoints disjoint
|
144
181
|
setpoints.difference_update(glyphspoints)
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
# A named glyph that can't be provided is an error
|
160
|
-
check_missing_glyphs(fileconf[CONF_PATH], glyphspoints)
|
161
|
-
# A missing glyph from a set is a warning.
|
162
|
-
if not config[CONF_IGNORE_MISSING_GLYPHS]:
|
163
|
-
check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True)
|
182
|
+
# check that glyphs are actually present
|
183
|
+
# Check extras against their own font, exclude from parent font codepoints
|
184
|
+
for extra in config[CONF_EXTRAS]:
|
185
|
+
points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
|
186
|
+
glyphspoints.difference_update(points)
|
187
|
+
setpoints.difference_update(points)
|
188
|
+
check_missing_glyphs(extra[CONF_FILE], points)
|
189
|
+
|
190
|
+
# A named glyph that can't be provided is an error
|
191
|
+
|
192
|
+
check_missing_glyphs(fileconf, glyphspoints)
|
193
|
+
# A missing glyph from a set is a warning.
|
194
|
+
if not config[CONF_IGNORE_MISSING_GLYPHS]:
|
195
|
+
check_missing_glyphs(fileconf, setpoints, warning=True)
|
164
196
|
|
165
197
|
# Populate the default after the above checks so that use of the default doesn't trigger errors
|
198
|
+
font = FONT_CACHE[fileconf]
|
166
199
|
if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]:
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
font
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
200
|
+
# set a default glyphset, intersected with what the font actually offers
|
201
|
+
config[CONF_GLYPHS] = [
|
202
|
+
chr(x)
|
203
|
+
for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
|
204
|
+
if font.get_char_index(x) != 0
|
205
|
+
]
|
206
|
+
|
207
|
+
if font.has_fixed_sizes:
|
208
|
+
sizes = [pt_to_px(x.size) for x in font.available_sizes]
|
209
|
+
if not sizes:
|
210
|
+
raise cv.Invalid(
|
211
|
+
f"Font {FontCache.get_name(fileconf)} has no available sizes"
|
212
|
+
)
|
213
|
+
if CONF_SIZE not in config:
|
214
|
+
config[CONF_SIZE] = sizes[0]
|
215
|
+
elif config[CONF_SIZE] not in sizes:
|
216
|
+
sizes = ", ".join(str(x) for x in sizes)
|
217
|
+
raise cv.Invalid(
|
218
|
+
f"Font {FontCache.get_name(fileconf)} only has size{'s' if len(sizes) != 1 else ''} {sizes} available"
|
219
|
+
)
|
220
|
+
elif CONF_SIZE not in config:
|
221
|
+
config[CONF_SIZE] = 20
|
177
222
|
|
178
223
|
return config
|
179
224
|
|
180
225
|
|
181
|
-
FONT_EXTENSIONS = (".ttf", ".woff", ".otf")
|
226
|
+
FONT_EXTENSIONS = (".ttf", ".woff", ".otf", "bdf", ".pcf")
|
182
227
|
|
183
228
|
|
184
229
|
def validate_truetype_file(value):
|
@@ -187,24 +232,30 @@ def validate_truetype_file(value):
|
|
187
232
|
f"Please unzip the font archive '{value}' first and then use the .ttf files inside."
|
188
233
|
)
|
189
234
|
if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
|
190
|
-
raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.")
|
235
|
+
raise cv.Invalid(f"Only {', '.join(FONT_EXTENSIONS)} files are supported.")
|
191
236
|
return CORE.relative_config_path(cv.file_(value))
|
192
237
|
|
193
238
|
|
239
|
+
def add_local_file(value):
|
240
|
+
if value in FONT_CACHE:
|
241
|
+
return value
|
242
|
+
path = value[CONF_PATH]
|
243
|
+
if not os.path.isfile(path):
|
244
|
+
raise cv.Invalid(f"File '{path}' not found.")
|
245
|
+
FONT_CACHE[value] = path
|
246
|
+
return value
|
247
|
+
|
248
|
+
|
194
249
|
TYPE_LOCAL = "local"
|
195
|
-
TYPE_LOCAL_BITMAP = "local_bitmap"
|
196
250
|
TYPE_GFONTS = "gfonts"
|
197
251
|
TYPE_WEB = "web"
|
198
|
-
LOCAL_SCHEMA = cv.
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
{
|
206
|
-
cv.Required(CONF_PATH): cv.file_,
|
207
|
-
}
|
252
|
+
LOCAL_SCHEMA = cv.All(
|
253
|
+
cv.Schema(
|
254
|
+
{
|
255
|
+
cv.Required(CONF_PATH): validate_truetype_file,
|
256
|
+
}
|
257
|
+
),
|
258
|
+
add_local_file,
|
208
259
|
)
|
209
260
|
|
210
261
|
FULLPATH_SCHEMA = cv.maybe_simple_value(
|
@@ -235,56 +286,59 @@ def _compute_local_font_path(value: dict) -> Path:
|
|
235
286
|
h.update(url.encode())
|
236
287
|
key = h.hexdigest()[:8]
|
237
288
|
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
238
|
-
_LOGGER.debug("_compute_local_font_path:
|
289
|
+
_LOGGER.debug("_compute_local_font_path: %s", base_dir / key)
|
239
290
|
return base_dir / key
|
240
291
|
|
241
292
|
|
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
293
|
def download_gfont(value):
|
294
|
+
if value in FONT_CACHE:
|
295
|
+
return value
|
252
296
|
name = (
|
253
297
|
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
|
254
298
|
)
|
255
299
|
url = f"https://fonts.googleapis.com/css2?family={name}"
|
256
|
-
path =
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
300
|
+
path = (
|
301
|
+
external_files.compute_local_file_dir(DOMAIN)
|
302
|
+
/ f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1.ttf"
|
303
|
+
)
|
304
|
+
if not external_files.is_file_recent(str(path), value[CONF_REFRESH]):
|
305
|
+
_LOGGER.debug("download_gfont: path=%s", path)
|
306
|
+
try:
|
307
|
+
req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
|
308
|
+
req.raise_for_status()
|
309
|
+
except requests.exceptions.RequestException as e:
|
310
|
+
raise cv.Invalid(
|
311
|
+
f"Could not download font at {url}, please check the fonts exists "
|
312
|
+
f"at google fonts ({e})"
|
313
|
+
)
|
314
|
+
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
|
315
|
+
if match is None:
|
316
|
+
raise cv.Invalid(
|
317
|
+
f"Could not extract ttf file from gfonts response for {name}, "
|
318
|
+
f"please report this."
|
319
|
+
)
|
273
320
|
|
274
|
-
|
275
|
-
|
321
|
+
ttf_url = match.group(1)
|
322
|
+
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
276
323
|
|
277
|
-
|
278
|
-
|
324
|
+
external_files.download_content(ttf_url, path)
|
325
|
+
# In case the remote file is not modified, the download_content function will return the existing file,
|
326
|
+
# so update the modification time to now.
|
327
|
+
path.touch()
|
328
|
+
FONT_CACHE[value] = path
|
329
|
+
return value
|
279
330
|
|
280
331
|
|
281
332
|
def download_web_font(value):
|
333
|
+
if value in FONT_CACHE:
|
334
|
+
return value
|
282
335
|
url = value[CONF_URL]
|
283
|
-
path =
|
336
|
+
path = _compute_local_font_path(value) / "font.ttf"
|
284
337
|
|
285
338
|
external_files.download_content(url, path)
|
286
339
|
_LOGGER.debug("download_web_font: path=%s", path)
|
287
|
-
|
340
|
+
FONT_CACHE[value] = path
|
341
|
+
return value
|
288
342
|
|
289
343
|
|
290
344
|
EXTERNAL_FONT_SCHEMA = cv.Schema(
|
@@ -340,15 +394,6 @@ def validate_file_shorthand(value):
|
|
340
394
|
}
|
341
395
|
)
|
342
396
|
|
343
|
-
if value.endswith(".pcf") or value.endswith(".bdf"):
|
344
|
-
value = convert_bitmap_to_pillow_font(
|
345
|
-
CORE.relative_config_path(cv.file_(value))
|
346
|
-
)
|
347
|
-
return {
|
348
|
-
CONF_TYPE: TYPE_LOCAL_BITMAP,
|
349
|
-
CONF_PATH: value,
|
350
|
-
}
|
351
|
-
|
352
397
|
return font_file_schema(
|
353
398
|
{
|
354
399
|
CONF_TYPE: TYPE_LOCAL,
|
@@ -361,7 +406,6 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
|
|
361
406
|
{
|
362
407
|
TYPE_LOCAL: LOCAL_SCHEMA,
|
363
408
|
TYPE_GFONTS: GFONTS_SCHEMA,
|
364
|
-
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
|
365
409
|
TYPE_WEB: WEB_FONT_SCHEMA,
|
366
410
|
}
|
367
411
|
)
|
@@ -391,7 +435,7 @@ FONT_SCHEMA = cv.Schema(
|
|
391
435
|
cv.one_of(*glyphsets.defined_glyphsets())
|
392
436
|
),
|
393
437
|
cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean,
|
394
|
-
cv.Optional(CONF_SIZE
|
438
|
+
cv.Optional(CONF_SIZE): cv.int_range(min=1),
|
395
439
|
cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8),
|
396
440
|
cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
|
397
441
|
cv.Schema(
|
@@ -406,114 +450,19 @@ FONT_SCHEMA = cv.Schema(
|
|
406
450
|
},
|
407
451
|
)
|
408
452
|
|
409
|
-
CONFIG_SCHEMA = cv.All(FONT_SCHEMA,
|
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
|
453
|
+
CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_font_config)
|
449
454
|
|
450
455
|
|
451
456
|
class EFont:
|
452
|
-
def __init__(self, file,
|
457
|
+
def __init__(self, file, codepoints):
|
453
458
|
self.codepoints = codepoints
|
454
|
-
|
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)
|
459
|
+
self.font: Face = FONT_CACHE[file]
|
512
460
|
|
513
461
|
|
514
462
|
class GlyphInfo:
|
515
|
-
def __init__(self, data_len, offset_x, offset_y, width, height):
|
463
|
+
def __init__(self, data_len, advance, offset_x, offset_y, width, height):
|
516
464
|
self.data_len = data_len
|
465
|
+
self.advance = advance
|
517
466
|
self.offset_x = offset_x
|
518
467
|
self.offset_y = offset_y
|
519
468
|
self.width = width
|
@@ -537,15 +486,14 @@ async def to_code(config):
|
|
537
486
|
}
|
538
487
|
# get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets
|
539
488
|
point_set.update(flatten(config[CONF_GLYPHS]))
|
540
|
-
size = config[CONF_SIZE]
|
541
489
|
# Create the codepoint to font file map
|
542
|
-
base_font =
|
543
|
-
point_font_map: dict[str,
|
490
|
+
base_font = FONT_CACHE[config[CONF_FILE]]
|
491
|
+
point_font_map: dict[str, Face] = {c: base_font for c in point_set}
|
544
492
|
# process extras, updating the map and extending the codepoint list
|
545
493
|
for extra in config[CONF_EXTRAS]:
|
546
494
|
extra_points = flatten(extra[CONF_GLYPHS])
|
547
495
|
point_set.update(extra_points)
|
548
|
-
extra_font =
|
496
|
+
extra_font = FONT_CACHE[extra[CONF_FILE]]
|
549
497
|
point_font_map.update({c: extra_font for c in extra_points})
|
550
498
|
|
551
499
|
codepoints = list(point_set)
|
@@ -553,28 +501,54 @@ async def to_code(config):
|
|
553
501
|
glyph_args = {}
|
554
502
|
data = []
|
555
503
|
bpp = config[CONF_BPP]
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
else:
|
560
|
-
mode = "L"
|
561
|
-
scale = 256 // (1 << bpp)
|
504
|
+
mode = ft_pixel_mode_grays
|
505
|
+
scale = 256 // (1 << bpp)
|
506
|
+
size = config[CONF_SIZE]
|
562
507
|
# create the data array for all glyphs
|
563
508
|
for codepoint in codepoints:
|
564
509
|
font = point_font_map[codepoint]
|
565
|
-
|
566
|
-
|
567
|
-
|
510
|
+
format = font.get_format().decode("utf-8")
|
511
|
+
if format != "PCF":
|
512
|
+
font.set_pixel_sizes(size, 0)
|
513
|
+
font.load_char(codepoint)
|
514
|
+
font.glyph.render(mode)
|
515
|
+
width = font.glyph.bitmap.width
|
516
|
+
height = font.glyph.bitmap.rows
|
517
|
+
buffer = font.glyph.bitmap.buffer
|
518
|
+
pitch = font.glyph.bitmap.pitch
|
568
519
|
glyph_data = [0] * ((height * width * bpp + 7) // 8)
|
520
|
+
src_mode = font.glyph.bitmap.pixel_mode
|
569
521
|
pos = 0
|
570
522
|
for y in range(height):
|
571
523
|
for x in range(width):
|
572
|
-
|
524
|
+
if src_mode == ft_pixel_mode_mono:
|
525
|
+
pixel = (
|
526
|
+
(1 << bpp) - 1
|
527
|
+
if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
|
528
|
+
else 0
|
529
|
+
)
|
530
|
+
else:
|
531
|
+
pixel = buffer[y * pitch + x] // scale
|
573
532
|
for bit_num in range(bpp):
|
574
533
|
if pixel & (1 << (bpp - bit_num - 1)):
|
575
534
|
glyph_data[pos // 8] |= 0x80 >> (pos % 8)
|
576
535
|
pos += 1
|
577
|
-
|
536
|
+
ascender = pt_to_px(font.size.ascender)
|
537
|
+
if ascender == 0:
|
538
|
+
if font.has_fixed_sizes:
|
539
|
+
ascender = size
|
540
|
+
else:
|
541
|
+
_LOGGER.error(
|
542
|
+
"Unable to determine ascender of font %s", config[CONF_FILE]
|
543
|
+
)
|
544
|
+
glyph_args[codepoint] = GlyphInfo(
|
545
|
+
len(data),
|
546
|
+
pt_to_px(font.glyph.metrics.horiAdvance),
|
547
|
+
font.glyph.bitmap_left,
|
548
|
+
ascender - font.glyph.bitmap_top,
|
549
|
+
width,
|
550
|
+
height,
|
551
|
+
)
|
578
552
|
data += glyph_data
|
579
553
|
|
580
554
|
rhs = [HexInt(x) for x in data]
|
@@ -598,6 +572,7 @@ async def to_code(config):
|
|
598
572
|
f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
|
599
573
|
),
|
600
574
|
),
|
575
|
+
("advance", glyph_args[codepoint].advance),
|
601
576
|
("offset_x", glyph_args[codepoint].offset_x),
|
602
577
|
("offset_y", glyph_args[codepoint].offset_y),
|
603
578
|
("width", glyph_args[codepoint].width),
|
@@ -607,11 +582,19 @@ async def to_code(config):
|
|
607
582
|
|
608
583
|
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
|
609
584
|
|
585
|
+
font_height = pt_to_px(base_font.size.height)
|
586
|
+
ascender = pt_to_px(base_font.size.ascender)
|
587
|
+
if font_height == 0:
|
588
|
+
if base_font.has_fixed_sizes:
|
589
|
+
font_height = size
|
590
|
+
ascender = font_height
|
591
|
+
else:
|
592
|
+
_LOGGER.error("Unable to determine height of font %s", config[CONF_FILE])
|
610
593
|
cg.new_Pvariable(
|
611
594
|
config[CONF_ID],
|
612
595
|
glyphs,
|
613
596
|
len(glyph_initializer),
|
614
|
-
|
615
|
-
|
597
|
+
ascender,
|
598
|
+
font_height,
|
616
599
|
bpp,
|
617
600
|
)
|
esphome/components/font/font.cpp
CHANGED
@@ -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_->
|
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_->
|
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_->
|
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_->
|
164
|
+
x_at += glyph.glyph_data_->advance;
|
165
165
|
|
166
166
|
i += match_length;
|
167
167
|
}
|
esphome/components/font/font.h
CHANGED
@@ -132,6 +132,10 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
|
|
132
132
|
yrange = ymax - ymin;
|
133
133
|
}
|
134
134
|
|
135
|
+
// Store graph limts
|
136
|
+
this->graph_limit_max_ = ymax;
|
137
|
+
this->graph_limit_min_ = ymin;
|
138
|
+
|
135
139
|
/// Draw grid
|
136
140
|
if (!std::isnan(this->gridspacing_y_)) {
|
137
141
|
for (int y = yn; y <= ym; y++) {
|
esphome/components/graph/graph.h
CHANGED
@@ -161,11 +161,15 @@ class Graph : public Component {
|
|
161
161
|
uint32_t get_duration() { return duration_; }
|
162
162
|
uint32_t get_width() { return width_; }
|
163
163
|
uint32_t get_height() { return height_; }
|
164
|
+
float get_graph_limit_min() { return graph_limit_min_; }
|
165
|
+
float get_graph_limit_max() { return graph_limit_max_; }
|
164
166
|
|
165
167
|
protected:
|
166
168
|
uint32_t duration_; /// in seconds
|
167
169
|
uint32_t width_; /// in pixels
|
168
170
|
uint32_t height_; /// in pixels
|
171
|
+
float graph_limit_min_{NAN};
|
172
|
+
float graph_limit_max_{NAN};
|
169
173
|
float min_value_{NAN};
|
170
174
|
float max_value_{NAN};
|
171
175
|
float min_range_{1.0};
|