esphome 2025.2.2__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.
- 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 +9 -0
- esphome/components/api/api_pb2.h +1 -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/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 +185 -185
- esphome/components/font/font.cpp +4 -4
- esphome/components/font/font.h +1 -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/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/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/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 +12 -4
- esphome/core/defines.h +1 -1
- esphome/core/helpers.h +5 -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.0b1.dist-info}/METADATA +5 -5
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/RECORD +134 -94
- esphome/components/cst816/binary_sensor/cst816_button.h +0 -27
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/LICENSE +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/WHEEL +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/entry_points.txt +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/top_level.txt +0 -0
@@ -1,28 +1,5 @@
|
|
1
|
-
import esphome.codegen as cg
|
2
1
|
import esphome.config_validation as cv
|
3
|
-
from esphome.components import binary_sensor
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
CONF_CST816_ID = "cst816_id"
|
9
|
-
|
10
|
-
CST816Button = cst816_ns.class_(
|
11
|
-
"CST816Button",
|
12
|
-
binary_sensor.BinarySensor,
|
13
|
-
cg.Component,
|
14
|
-
CST816ButtonListener,
|
15
|
-
cg.Parented.template(CST816Touchscreen),
|
16
|
-
)
|
17
|
-
|
18
|
-
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST816Button).extend(
|
19
|
-
{
|
20
|
-
cv.GenerateID(CONF_CST816_ID): cv.use_id(CST816Touchscreen),
|
21
|
-
}
|
3
|
+
CONFIG_SCHEMA = cv.invalid(
|
4
|
+
"The CST816 binary sensor has been removed. Instead use the touchscreen binary sensor with the 'use_raw' flag set."
|
22
5
|
)
|
23
|
-
|
24
|
-
|
25
|
-
async def to_code(config):
|
26
|
-
var = await binary_sensor.new_binary_sensor(config)
|
27
|
-
await cg.register_component(var, config)
|
28
|
-
await cg.register_parented(var, config[CONF_CST816_ID])
|
@@ -37,14 +37,6 @@ void CST816Touchscreen::continue_setup_() {
|
|
37
37
|
ESP_LOGCONFIG(TAG, "CST816 Touchscreen setup complete");
|
38
38
|
}
|
39
39
|
|
40
|
-
void CST816Touchscreen::update_button_state_(bool state) {
|
41
|
-
if (this->button_touched_ == state)
|
42
|
-
return;
|
43
|
-
this->button_touched_ = state;
|
44
|
-
for (auto *listener : this->button_listeners_)
|
45
|
-
listener->update_button(state);
|
46
|
-
}
|
47
|
-
|
48
40
|
void CST816Touchscreen::setup() {
|
49
41
|
ESP_LOGCONFIG(TAG, "Setting up CST816 Touchscreen...");
|
50
42
|
if (this->reset_pin_ != nullptr) {
|
@@ -68,18 +60,13 @@ void CST816Touchscreen::update_touches() {
|
|
68
60
|
}
|
69
61
|
uint8_t num_of_touches = data[REG_TOUCH_NUM] & 3;
|
70
62
|
if (num_of_touches == 0) {
|
71
|
-
this->update_button_state_(false);
|
72
63
|
return;
|
73
64
|
}
|
74
65
|
|
75
66
|
uint16_t x = encode_uint16(data[REG_XPOS_HIGH] & 0xF, data[REG_XPOS_LOW]);
|
76
67
|
uint16_t y = encode_uint16(data[REG_YPOS_HIGH] & 0xF, data[REG_YPOS_LOW]);
|
77
68
|
ESP_LOGV(TAG, "Read touch %d/%d", x, y);
|
78
|
-
|
79
|
-
this->update_button_state_(true);
|
80
|
-
} else {
|
81
|
-
this->add_raw_touch_position_(0, x, y);
|
82
|
-
}
|
69
|
+
this->add_raw_touch_position_(0, x, y);
|
83
70
|
}
|
84
71
|
|
85
72
|
void CST816Touchscreen::dump_config() {
|
@@ -87,6 +74,8 @@ void CST816Touchscreen::dump_config() {
|
|
87
74
|
LOG_I2C_DEVICE(this);
|
88
75
|
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
89
76
|
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
77
|
+
ESP_LOGCONFIG(TAG, " X Raw Min: %d, X Raw Max: %d", this->x_raw_min_, this->x_raw_max_);
|
78
|
+
ESP_LOGCONFIG(TAG, " Y Raw Min: %d, Y Raw Max: %d", this->y_raw_min_, this->y_raw_max_);
|
90
79
|
const char *name;
|
91
80
|
switch (this->chip_id_) {
|
92
81
|
case CST820_CHIP_ID:
|
@@ -40,7 +40,6 @@ class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
|
40
40
|
public:
|
41
41
|
void setup() override;
|
42
42
|
void update_touches() override;
|
43
|
-
void register_button_listener(CST816ButtonListener *listener) { this->button_listeners_.push_back(listener); }
|
44
43
|
void dump_config() override;
|
45
44
|
|
46
45
|
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
@@ -49,14 +48,11 @@ class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
|
49
48
|
|
50
49
|
protected:
|
51
50
|
void continue_setup_();
|
52
|
-
void update_button_state_(bool state);
|
53
51
|
|
54
52
|
InternalGPIOPin *interrupt_pin_{};
|
55
53
|
GPIOPin *reset_pin_{};
|
56
54
|
uint8_t chip_id_{};
|
57
55
|
bool skip_probe_{}; // if set, do not expect to be able to probe the controller on the i2c bus.
|
58
|
-
std::vector<CST816ButtonListener *> button_listeners_;
|
59
|
-
bool button_touched_{};
|
60
56
|
};
|
61
57
|
|
62
58
|
} // namespace cst816
|
@@ -66,7 +66,9 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
|
|
66
66
|
|
67
67
|
async def to_code(config):
|
68
68
|
uuid = config[CONF_UUID].hex
|
69
|
-
uuid_arr = [
|
69
|
+
uuid_arr = [
|
70
|
+
cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2)
|
71
|
+
]
|
70
72
|
var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
|
71
73
|
|
72
74
|
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
|
@@ -112,8 +112,7 @@ def validate_supports(value):
|
|
112
112
|
)
|
113
113
|
if is_pullup and num == 16:
|
114
114
|
raise cv.Invalid(
|
115
|
-
"GPIO Pin 16 does not support pullup pin mode. "
|
116
|
-
"Please choose another pin.",
|
115
|
+
"GPIO Pin 16 does not support pullup pin mode. Please choose another pin.",
|
117
116
|
[CONF_MODE, CONF_PULLUP],
|
118
117
|
)
|
119
118
|
if is_pulldown and num != 16:
|
@@ -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,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 {
|
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 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]
|
185
|
+
check_missing_glyphs(extra[CONF_FILE], points)
|
158
186
|
|
159
187
|
# A named glyph that can't be provided is an error
|
160
|
-
|
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
|
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
|
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.
|
199
|
-
|
200
|
-
|
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.
|
205
|
-
|
206
|
-
|
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:
|
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 =
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
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
|
-
|
275
|
-
|
330
|
+
ttf_url = match.group(1)
|
331
|
+
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
|
276
332
|
|
277
|
-
|
278
|
-
|
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 =
|
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
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
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
|
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,
|
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,
|
476
|
+
def __init__(self, file, codepoints):
|
453
477
|
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)
|
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 =
|
543
|
-
point_font_map: dict[str,
|
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 =
|
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
|
-
|
557
|
-
|
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
|
-
|
566
|
-
|
567
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
615
|
-
|
614
|
+
ascender,
|
615
|
+
font_height,
|
616
616
|
bpp,
|
617
617
|
)
|