esphome 2025.4.0b1__py3-none-any.whl → 2025.4.0b3__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 (32) hide show
  1. esphome/__main__.py +5 -3
  2. esphome/components/am2315c/am2315c.cpp +4 -4
  3. esphome/components/api/api_frame_helper.cpp +4 -0
  4. esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +7 -3
  5. esphome/components/lvgl/automation.py +14 -2
  6. esphome/components/lvgl/encoders.py +3 -2
  7. esphome/components/lvgl/lvcode.py +2 -1
  8. esphome/components/lvgl/lvgl_esphome.h +2 -0
  9. esphome/components/lvgl/number/__init__.py +23 -20
  10. esphome/components/lvgl/number/lvgl_number.h +27 -14
  11. esphome/components/lvgl/schemas.py +3 -1
  12. esphome/components/lvgl/select/__init__.py +11 -13
  13. esphome/components/lvgl/select/lvgl_select.h +25 -18
  14. esphome/components/lvgl/widgets/buttonmatrix.py +2 -2
  15. esphome/components/lvgl/widgets/canvas.py +7 -3
  16. esphome/components/lvgl/widgets/meter.py +3 -1
  17. esphome/components/sml/sml.cpp +8 -6
  18. esphome/components/sml/sml.h +1 -1
  19. esphome/components/sml/sml_parser.cpp +16 -18
  20. esphome/components/sml/sml_parser.h +42 -12
  21. esphome/components/speaker/media_player/audio_pipeline.cpp +3 -2
  22. esphome/config_validation.py +0 -42
  23. esphome/const.py +1 -1
  24. esphome/core/__init__.py +0 -1
  25. esphome/vscode.py +27 -8
  26. esphome/yaml_util.py +75 -39
  27. {esphome-2025.4.0b1.dist-info → esphome-2025.4.0b3.dist-info}/METADATA +4 -4
  28. {esphome-2025.4.0b1.dist-info → esphome-2025.4.0b3.dist-info}/RECORD +32 -32
  29. {esphome-2025.4.0b1.dist-info → esphome-2025.4.0b3.dist-info}/WHEEL +0 -0
  30. {esphome-2025.4.0b1.dist-info → esphome-2025.4.0b3.dist-info}/entry_points.txt +0 -0
  31. {esphome-2025.4.0b1.dist-info → esphome-2025.4.0b3.dist-info}/licenses/LICENSE +0 -0
  32. {esphome-2025.4.0b1.dist-info → esphome-2025.4.0b3.dist-info}/top_level.txt +0 -0
@@ -5,17 +5,17 @@
5
5
  namespace esphome {
6
6
  namespace sml {
7
7
 
8
- SmlFile::SmlFile(bytes buffer) : buffer_(std::move(buffer)) {
8
+ SmlFile::SmlFile(const BytesView &buffer) : buffer_(buffer) {
9
9
  // extract messages
10
10
  this->pos_ = 0;
11
11
  while (this->pos_ < this->buffer_.size()) {
12
12
  if (this->buffer_[this->pos_] == 0x00)
13
13
  break; // EndOfSmlMsg
14
14
 
15
- SmlNode message = SmlNode();
15
+ SmlNode message;
16
16
  if (!this->setup_node(&message))
17
17
  break;
18
- this->messages.emplace_back(message);
18
+ this->messages.emplace_back(std::move(message));
19
19
  }
20
20
  }
21
21
 
@@ -62,22 +62,20 @@ bool SmlFile::setup_node(SmlNode *node) {
62
62
  return false;
63
63
 
64
64
  node->type = type;
65
- node->nodes.clear();
66
- node->value_bytes.clear();
67
65
 
68
66
  if (type == SML_LIST) {
69
67
  node->nodes.reserve(length);
70
68
  for (size_t i = 0; i != length; i++) {
71
- SmlNode child_node = SmlNode();
69
+ SmlNode child_node;
72
70
  if (!this->setup_node(&child_node))
73
71
  return false;
74
- node->nodes.emplace_back(child_node);
72
+ node->nodes.emplace_back(std::move(child_node));
75
73
  }
76
74
  } else {
77
75
  // Value starts at the current position
78
76
  // Value ends "length" bytes later,
79
77
  // (since the TL field is counted but already subtracted from length)
80
- node->value_bytes = bytes(this->buffer_.begin() + this->pos_, this->buffer_.begin() + this->pos_ + length);
78
+ node->value_bytes = buffer_.subview(this->pos_, length);
81
79
  // Increment the pointer past all consumed bytes
82
80
  this->pos_ += length;
83
81
  }
@@ -87,14 +85,14 @@ bool SmlFile::setup_node(SmlNode *node) {
87
85
  std::vector<ObisInfo> SmlFile::get_obis_info() {
88
86
  std::vector<ObisInfo> obis_info;
89
87
  for (auto const &message : messages) {
90
- SmlNode message_body = message.nodes[3];
88
+ const auto &message_body = message.nodes[3];
91
89
  uint16_t message_type = bytes_to_uint(message_body.nodes[0].value_bytes);
92
90
  if (message_type != SML_GET_LIST_RES)
93
91
  continue;
94
92
 
95
- SmlNode get_list_response = message_body.nodes[1];
96
- bytes server_id = get_list_response.nodes[1].value_bytes;
97
- SmlNode val_list = get_list_response.nodes[4];
93
+ const auto &get_list_response = message_body.nodes[1];
94
+ const auto &server_id = get_list_response.nodes[1].value_bytes;
95
+ const auto &val_list = get_list_response.nodes[4];
98
96
 
99
97
  for (auto const &val_list_entry : val_list.nodes) {
100
98
  obis_info.emplace_back(server_id, val_list_entry);
@@ -103,7 +101,7 @@ std::vector<ObisInfo> SmlFile::get_obis_info() {
103
101
  return obis_info;
104
102
  }
105
103
 
106
- std::string bytes_repr(const bytes &buffer) {
104
+ std::string bytes_repr(const BytesView &buffer) {
107
105
  std::string repr;
108
106
  for (auto const value : buffer) {
109
107
  repr += str_sprintf("%02x", value & 0xff);
@@ -111,7 +109,7 @@ std::string bytes_repr(const bytes &buffer) {
111
109
  return repr;
112
110
  }
113
111
 
114
- uint64_t bytes_to_uint(const bytes &buffer) {
112
+ uint64_t bytes_to_uint(const BytesView &buffer) {
115
113
  uint64_t val = 0;
116
114
  for (auto const value : buffer) {
117
115
  val = (val << 8) + value;
@@ -119,7 +117,7 @@ uint64_t bytes_to_uint(const bytes &buffer) {
119
117
  return val;
120
118
  }
121
119
 
122
- int64_t bytes_to_int(const bytes &buffer) {
120
+ int64_t bytes_to_int(const BytesView &buffer) {
123
121
  uint64_t tmp = bytes_to_uint(buffer);
124
122
  int64_t val;
125
123
 
@@ -135,14 +133,14 @@ int64_t bytes_to_int(const bytes &buffer) {
135
133
  return val;
136
134
  }
137
135
 
138
- std::string bytes_to_string(const bytes &buffer) { return std::string(buffer.begin(), buffer.end()); }
136
+ std::string bytes_to_string(const BytesView &buffer) { return std::string(buffer.begin(), buffer.end()); }
139
137
 
140
- ObisInfo::ObisInfo(bytes server_id, SmlNode val_list_entry) : server_id(std::move(server_id)) {
138
+ ObisInfo::ObisInfo(const BytesView &server_id, const SmlNode &val_list_entry) : server_id(server_id) {
141
139
  this->code = val_list_entry.nodes[0].value_bytes;
142
140
  this->status = val_list_entry.nodes[1].value_bytes;
143
141
  this->unit = bytes_to_uint(val_list_entry.nodes[3].value_bytes);
144
142
  this->scaler = bytes_to_int(val_list_entry.nodes[4].value_bytes);
145
- SmlNode value_node = val_list_entry.nodes[5];
143
+ const auto &value_node = val_list_entry.nodes[5];
146
144
  this->value = value_node.value_bytes;
147
145
  this->value_type = value_node.type;
148
146
  }
@@ -1,5 +1,6 @@
1
1
  #pragma once
2
2
 
3
+ #include <cassert>
3
4
  #include <cstdint>
4
5
  #include <cstdio>
5
6
  #include <string>
@@ -11,44 +12,73 @@ namespace sml {
11
12
 
12
13
  using bytes = std::vector<uint8_t>;
13
14
 
15
+ class BytesView {
16
+ public:
17
+ BytesView() noexcept = default;
18
+
19
+ explicit BytesView(const uint8_t *first, size_t count) noexcept : data_{first}, count_{count} {}
20
+
21
+ explicit BytesView(const bytes &bytes) noexcept : data_{bytes.data()}, count_{bytes.size()} {}
22
+
23
+ size_t size() const noexcept { return count_; }
24
+
25
+ uint8_t operator[](size_t index) const noexcept {
26
+ assert(index < count_);
27
+ return data_[index];
28
+ }
29
+
30
+ BytesView subview(size_t offset, size_t count) const noexcept {
31
+ assert(offset + count <= count_);
32
+ return BytesView{data_ + offset, count};
33
+ }
34
+
35
+ const uint8_t *begin() const noexcept { return data_; }
36
+
37
+ const uint8_t *end() const noexcept { return data_ + count_; }
38
+
39
+ private:
40
+ const uint8_t *data_ = nullptr;
41
+ size_t count_ = 0;
42
+ };
43
+
14
44
  class SmlNode {
15
45
  public:
16
46
  uint8_t type;
17
- bytes value_bytes;
47
+ BytesView value_bytes;
18
48
  std::vector<SmlNode> nodes;
19
49
  };
20
50
 
21
51
  class ObisInfo {
22
52
  public:
23
- ObisInfo(bytes server_id, SmlNode val_list_entry);
24
- bytes server_id;
25
- bytes code;
26
- bytes status;
53
+ ObisInfo(const BytesView &server_id, const SmlNode &val_list_entry);
54
+ BytesView server_id;
55
+ BytesView code;
56
+ BytesView status;
27
57
  char unit;
28
58
  char scaler;
29
- bytes value;
59
+ BytesView value;
30
60
  uint16_t value_type;
31
61
  std::string code_repr() const;
32
62
  };
33
63
 
34
64
  class SmlFile {
35
65
  public:
36
- SmlFile(bytes buffer);
66
+ SmlFile(const BytesView &buffer);
37
67
  bool setup_node(SmlNode *node);
38
68
  std::vector<SmlNode> messages;
39
69
  std::vector<ObisInfo> get_obis_info();
40
70
 
41
71
  protected:
42
- const bytes buffer_;
72
+ const BytesView buffer_;
43
73
  size_t pos_;
44
74
  };
45
75
 
46
- std::string bytes_repr(const bytes &buffer);
76
+ std::string bytes_repr(const BytesView &buffer);
47
77
 
48
- uint64_t bytes_to_uint(const bytes &buffer);
78
+ uint64_t bytes_to_uint(const BytesView &buffer);
49
79
 
50
- int64_t bytes_to_int(const bytes &buffer);
80
+ int64_t bytes_to_int(const BytesView &buffer);
51
81
 
52
- std::string bytes_to_string(const bytes &buffer);
82
+ std::string bytes_to_string(const BytesView &buffer);
53
83
  } // namespace sml
54
84
  } // namespace esphome
@@ -441,9 +441,10 @@ void AudioPipeline::decode_task(void *params) {
441
441
  pdFALSE, // Wait for all the bits,
442
442
  portMAX_DELAY); // Block indefinitely until bit is set
443
443
 
444
+ xEventGroupClearBits(this_pipeline->event_group_,
445
+ EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
446
+
444
447
  if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
445
- xEventGroupClearBits(this_pipeline->event_group_,
446
- EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
447
448
  InfoErrorEvent event;
448
449
  event.source = InfoErrorSource::DECODER;
449
450
 
@@ -1499,30 +1499,9 @@ def dimensions(value):
1499
1499
 
1500
1500
 
1501
1501
  def directory(value):
1502
- import json
1503
-
1504
1502
  value = string(value)
1505
1503
  path = CORE.relative_config_path(value)
1506
1504
 
1507
- if CORE.vscode and (
1508
- not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
1509
- ):
1510
- print(
1511
- json.dumps(
1512
- {
1513
- "type": "check_directory_exists",
1514
- "path": path,
1515
- }
1516
- )
1517
- )
1518
- data = json.loads(input())
1519
- assert data["type"] == "directory_exists_response"
1520
- if data["content"]:
1521
- return value
1522
- raise Invalid(
1523
- f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."
1524
- )
1525
-
1526
1505
  if not os.path.exists(path):
1527
1506
  raise Invalid(
1528
1507
  f"Could not find directory '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."
@@ -1535,30 +1514,9 @@ def directory(value):
1535
1514
 
1536
1515
 
1537
1516
  def file_(value):
1538
- import json
1539
-
1540
1517
  value = string(value)
1541
1518
  path = CORE.relative_config_path(value)
1542
1519
 
1543
- if CORE.vscode and (
1544
- not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)
1545
- ):
1546
- print(
1547
- json.dumps(
1548
- {
1549
- "type": "check_file_exists",
1550
- "path": path,
1551
- }
1552
- )
1553
- )
1554
- data = json.loads(input())
1555
- assert data["type"] == "file_exists_response"
1556
- if data["content"]:
1557
- return value
1558
- raise Invalid(
1559
- f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."
1560
- )
1561
-
1562
1520
  if not os.path.exists(path):
1563
1521
  raise Invalid(
1564
1522
  f"Could not find file '{path}'. Please make sure it exists (full path: {os.path.abspath(path)})."
esphome/const.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Constants used by esphome."""
2
2
 
3
- __version__ = "2025.4.0b1"
3
+ __version__ = "2025.4.0b3"
4
4
 
5
5
  ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
6
6
  VALID_SUBSTITUTIONS_CHARACTERS = (
esphome/core/__init__.py CHANGED
@@ -475,7 +475,6 @@ class EsphomeCore:
475
475
  self.dashboard = False
476
476
  # True if command is run from vscode api
477
477
  self.vscode = False
478
- self.ace = False
479
478
  # The name of the node
480
479
  self.name: Optional[str] = None
481
480
  # The friendly name of the node
esphome/vscode.py CHANGED
@@ -78,28 +78,47 @@ def _print_file_read_event(path: str) -> None:
78
78
  )
79
79
 
80
80
 
81
+ def _request_and_get_stream_on_stdin(fname: str) -> StringIO:
82
+ _print_file_read_event(fname)
83
+ raw_yaml_stream = StringIO(_read_file_content_from_json_on_stdin())
84
+ return raw_yaml_stream
85
+
86
+
87
+ def _vscode_loader(fname: str) -> dict[str, Any]:
88
+ raw_yaml_stream = _request_and_get_stream_on_stdin(fname)
89
+ # it is required to set the name on StringIO so document on start_mark
90
+ # is set properly. Otherwise it is initialized with "<file>"
91
+ raw_yaml_stream.name = fname
92
+ return parse_yaml(fname, raw_yaml_stream, _vscode_loader)
93
+
94
+
95
+ def _ace_loader(fname: str) -> dict[str, Any]:
96
+ raw_yaml_stream = _request_and_get_stream_on_stdin(fname)
97
+ return parse_yaml(fname, raw_yaml_stream)
98
+
99
+
81
100
  def read_config(args):
82
101
  while True:
83
102
  CORE.reset()
84
103
  data = json.loads(input())
85
- assert data["type"] == "validate"
104
+ assert data["type"] == "validate" or data["type"] == "exit"
105
+ if data["type"] == "exit":
106
+ return
86
107
  CORE.vscode = True
87
- CORE.ace = args.ace
88
- f = data["file"]
89
- if CORE.ace:
90
- CORE.config_path = os.path.join(args.configuration, f)
108
+ if args.ace: # Running from ESPHome Compiler dashboard, not vscode
109
+ CORE.config_path = os.path.join(args.configuration, data["file"])
110
+ loader = _ace_loader
91
111
  else:
92
112
  CORE.config_path = data["file"]
113
+ loader = _vscode_loader
93
114
 
94
115
  file_name = CORE.config_path
95
- _print_file_read_event(file_name)
96
- raw_yaml = _read_file_content_from_json_on_stdin()
97
116
  command_line_substitutions: dict[str, Any] = (
98
117
  dict(args.substitution) if args.substitution else {}
99
118
  )
100
119
  vs = VSCodeResult()
101
120
  try:
102
- config = parse_yaml(file_name, StringIO(raw_yaml))
121
+ config = loader(file_name)
103
122
  res = validate_config(config, command_line_substitutions)
104
123
  except Exception as err: # pylint: disable=broad-except
105
124
  vs.add_yaml_error(str(err))
esphome/yaml_util.py CHANGED
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  import fnmatch
4
4
  import functools
5
5
  import inspect
6
- from io import TextIOWrapper
6
+ from io import BytesIO, TextIOBase, TextIOWrapper
7
7
  from ipaddress import _BaseAddress
8
8
  import logging
9
9
  import math
10
10
  import os
11
- from typing import Any
11
+ from typing import Any, Callable
12
12
  import uuid
13
13
 
14
14
  import yaml
@@ -69,7 +69,10 @@ class ESPForceValue:
69
69
  pass
70
70
 
71
71
 
72
- def make_data_base(value, from_database: ESPHomeDataBase = None):
72
+ def make_data_base(
73
+ value, from_database: ESPHomeDataBase = None
74
+ ) -> ESPHomeDataBase | Any:
75
+ """Wrap a value in a ESPHomeDataBase object."""
73
76
  try:
74
77
  value = add_class_to_obj(value, ESPHomeDataBase)
75
78
  if from_database is not None:
@@ -102,6 +105,11 @@ def _add_data_ref(fn):
102
105
  class ESPHomeLoaderMixin:
103
106
  """Loader class that keeps track of line numbers."""
104
107
 
108
+ def __init__(self, name: str, yaml_loader: Callable[[str], dict[str, Any]]) -> None:
109
+ """Initialize the loader."""
110
+ self.name = name
111
+ self.yaml_loader = yaml_loader
112
+
105
113
  @_add_data_ref
106
114
  def construct_yaml_int(self, node):
107
115
  return super().construct_yaml_int(node)
@@ -127,7 +135,7 @@ class ESPHomeLoaderMixin:
127
135
  return super().construct_yaml_seq(node)
128
136
 
129
137
  @_add_data_ref
130
- def construct_yaml_map(self, node):
138
+ def construct_yaml_map(self, node: yaml.MappingNode) -> OrderedDict[str, Any]:
131
139
  """Traverses the given mapping node and returns a list of constructed key-value pairs."""
132
140
  assert isinstance(node, yaml.MappingNode)
133
141
  # A list of key-value pairs we find in the current mapping
@@ -231,7 +239,7 @@ class ESPHomeLoaderMixin:
231
239
  return OrderedDict(pairs)
232
240
 
233
241
  @_add_data_ref
234
- def construct_env_var(self, node):
242
+ def construct_env_var(self, node: yaml.Node) -> str:
235
243
  args = node.value.split()
236
244
  # Check for a default value
237
245
  if len(args) > 1:
@@ -243,23 +251,23 @@ class ESPHomeLoaderMixin:
243
251
  )
244
252
 
245
253
  @property
246
- def _directory(self):
254
+ def _directory(self) -> str:
247
255
  return os.path.dirname(self.name)
248
256
 
249
- def _rel_path(self, *args):
257
+ def _rel_path(self, *args: str) -> str:
250
258
  return os.path.join(self._directory, *args)
251
259
 
252
260
  @_add_data_ref
253
- def construct_secret(self, node):
261
+ def construct_secret(self, node: yaml.Node) -> str:
254
262
  try:
255
- secrets = _load_yaml_internal(self._rel_path(SECRET_YAML))
263
+ secrets = self.yaml_loader(self._rel_path(SECRET_YAML))
256
264
  except EsphomeError as e:
257
265
  if self.name == CORE.config_path:
258
266
  raise e
259
267
  try:
260
268
  main_config_dir = os.path.dirname(CORE.config_path)
261
269
  main_secret_yml = os.path.join(main_config_dir, SECRET_YAML)
262
- secrets = _load_yaml_internal(main_secret_yml)
270
+ secrets = self.yaml_loader(main_secret_yml)
263
271
  except EsphomeError as er:
264
272
  raise EsphomeError(f"{e}\n{er}") from er
265
273
 
@@ -272,7 +280,9 @@ class ESPHomeLoaderMixin:
272
280
  return val
273
281
 
274
282
  @_add_data_ref
275
- def construct_include(self, node):
283
+ def construct_include(
284
+ self, node: yaml.Node
285
+ ) -> dict[str, Any] | OrderedDict[str, Any]:
276
286
  from esphome.const import CONF_VARS
277
287
 
278
288
  def extract_file_vars(node):
@@ -290,71 +300,93 @@ class ESPHomeLoaderMixin:
290
300
  else:
291
301
  file, vars = node.value, None
292
302
 
293
- result = _load_yaml_internal(self._rel_path(file))
303
+ result = self.yaml_loader(self._rel_path(file))
294
304
  if not vars:
295
305
  vars = {}
296
306
  result = substitute_vars(result, vars)
297
307
  return result
298
308
 
299
309
  @_add_data_ref
300
- def construct_include_dir_list(self, node):
310
+ def construct_include_dir_list(self, node: yaml.Node) -> list[dict[str, Any]]:
301
311
  files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
302
- return [_load_yaml_internal(f) for f in files]
312
+ return [self.yaml_loader(f) for f in files]
303
313
 
304
314
  @_add_data_ref
305
- def construct_include_dir_merge_list(self, node):
315
+ def construct_include_dir_merge_list(self, node: yaml.Node) -> list[dict[str, Any]]:
306
316
  files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
307
317
  merged_list = []
308
318
  for fname in files:
309
- loaded_yaml = _load_yaml_internal(fname)
319
+ loaded_yaml = self.yaml_loader(fname)
310
320
  if isinstance(loaded_yaml, list):
311
321
  merged_list.extend(loaded_yaml)
312
322
  return merged_list
313
323
 
314
324
  @_add_data_ref
315
- def construct_include_dir_named(self, node):
325
+ def construct_include_dir_named(
326
+ self, node: yaml.Node
327
+ ) -> OrderedDict[str, dict[str, Any]]:
316
328
  files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
317
329
  mapping = OrderedDict()
318
330
  for fname in files:
319
331
  filename = os.path.splitext(os.path.basename(fname))[0]
320
- mapping[filename] = _load_yaml_internal(fname)
332
+ mapping[filename] = self.yaml_loader(fname)
321
333
  return mapping
322
334
 
323
335
  @_add_data_ref
324
- def construct_include_dir_merge_named(self, node):
336
+ def construct_include_dir_merge_named(
337
+ self, node: yaml.Node
338
+ ) -> OrderedDict[str, dict[str, Any]]:
325
339
  files = filter_yaml_files(_find_files(self._rel_path(node.value), "*.yaml"))
326
340
  mapping = OrderedDict()
327
341
  for fname in files:
328
- loaded_yaml = _load_yaml_internal(fname)
342
+ loaded_yaml = self.yaml_loader(fname)
329
343
  if isinstance(loaded_yaml, dict):
330
344
  mapping.update(loaded_yaml)
331
345
  return mapping
332
346
 
333
347
  @_add_data_ref
334
- def construct_lambda(self, node):
348
+ def construct_lambda(self, node: yaml.Node) -> Lambda:
335
349
  return Lambda(str(node.value))
336
350
 
337
351
  @_add_data_ref
338
- def construct_force(self, node):
352
+ def construct_force(self, node: yaml.Node) -> ESPForceValue:
339
353
  obj = self.construct_scalar(node)
340
354
  return add_class_to_obj(obj, ESPForceValue)
341
355
 
342
356
  @_add_data_ref
343
- def construct_extend(self, node):
357
+ def construct_extend(self, node: yaml.Node) -> Extend:
344
358
  return Extend(str(node.value))
345
359
 
346
360
  @_add_data_ref
347
- def construct_remove(self, node):
361
+ def construct_remove(self, node: yaml.Node) -> Remove:
348
362
  return Remove(str(node.value))
349
363
 
350
364
 
351
365
  class ESPHomeLoader(ESPHomeLoaderMixin, FastestAvailableSafeLoader):
352
366
  """Loader class that keeps track of line numbers."""
353
367
 
368
+ def __init__(
369
+ self,
370
+ stream: TextIOBase | BytesIO,
371
+ name: str,
372
+ yaml_loader: Callable[[str], dict[str, Any]],
373
+ ) -> None:
374
+ FastestAvailableSafeLoader.__init__(self, stream)
375
+ ESPHomeLoaderMixin.__init__(self, name, yaml_loader)
376
+
354
377
 
355
378
  class ESPHomePurePythonLoader(ESPHomeLoaderMixin, PurePythonLoader):
356
379
  """Loader class that keeps track of line numbers."""
357
380
 
381
+ def __init__(
382
+ self,
383
+ stream: TextIOBase | BytesIO,
384
+ name: str,
385
+ yaml_loader: Callable[[str], dict[str, Any]],
386
+ ) -> None:
387
+ PurePythonLoader.__init__(self, stream)
388
+ ESPHomeLoaderMixin.__init__(self, name, yaml_loader)
389
+
358
390
 
359
391
  for _loader in (ESPHomeLoader, ESPHomePurePythonLoader):
360
392
  _loader.add_constructor("tag:yaml.org,2002:int", _loader.construct_yaml_int)
@@ -388,17 +420,30 @@ def load_yaml(fname: str, clear_secrets: bool = True) -> Any:
388
420
  return _load_yaml_internal(fname)
389
421
 
390
422
 
391
- def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any:
423
+ def _load_yaml_internal(fname: str) -> Any:
424
+ """Load a YAML file."""
425
+ try:
426
+ with open(fname, encoding="utf-8") as f_handle:
427
+ return parse_yaml(fname, f_handle)
428
+ except (UnicodeDecodeError, OSError) as err:
429
+ raise EsphomeError(f"Error reading file {fname}: {err}") from err
430
+
431
+
432
+ def parse_yaml(
433
+ file_name: str, file_handle: TextIOWrapper, yaml_loader=_load_yaml_internal
434
+ ) -> Any:
392
435
  """Parse a YAML file."""
393
436
  try:
394
- return _load_yaml_internal_with_type(ESPHomeLoader, file_name, file_handle)
437
+ return _load_yaml_internal_with_type(
438
+ ESPHomeLoader, file_name, file_handle, yaml_loader
439
+ )
395
440
  except EsphomeError:
396
441
  # Loading failed, so we now load with the Python loader which has more
397
442
  # readable exceptions
398
443
  # Rewind the stream so we can try again
399
444
  file_handle.seek(0, 0)
400
445
  return _load_yaml_internal_with_type(
401
- ESPHomePurePythonLoader, file_name, file_handle
446
+ ESPHomePurePythonLoader, file_name, file_handle, yaml_loader
402
447
  )
403
448
 
404
449
 
@@ -435,23 +480,14 @@ def substitute_vars(config, vars):
435
480
  return result
436
481
 
437
482
 
438
- def _load_yaml_internal(fname: str) -> Any:
439
- """Load a YAML file."""
440
- try:
441
- with open(fname, encoding="utf-8") as f_handle:
442
- return parse_yaml(fname, f_handle)
443
- except (UnicodeDecodeError, OSError) as err:
444
- raise EsphomeError(f"Error reading file {fname}: {err}") from err
445
-
446
-
447
483
  def _load_yaml_internal_with_type(
448
484
  loader_type: type[ESPHomeLoader] | type[ESPHomePurePythonLoader],
449
485
  fname: str,
450
486
  content: TextIOWrapper,
487
+ yaml_loader: Any,
451
488
  ) -> Any:
452
489
  """Load a YAML file."""
453
- loader = loader_type(content)
454
- loader.name = fname
490
+ loader = loader_type(content, fname, yaml_loader)
455
491
  try:
456
492
  return loader.get_single_data() or OrderedDict()
457
493
  except yaml.YAMLError as exc:
@@ -470,7 +506,7 @@ def dump(dict_, show_secrets=False):
470
506
  )
471
507
 
472
508
 
473
- def _is_file_valid(name):
509
+ def _is_file_valid(name: str) -> bool:
474
510
  """Decide if a file is valid."""
475
511
  return not name.startswith(".")
476
512
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: esphome
3
- Version: 2025.4.0b1
3
+ Version: 2025.4.0b3
4
4
  Summary: ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems.
5
5
  Author-email: The ESPHome Authors <esphome@openhomefoundation.org>
6
6
  License: MIT
@@ -37,9 +37,9 @@ Requires-Dist: pyserial==3.5
37
37
  Requires-Dist: platformio==6.1.18
38
38
  Requires-Dist: esptool==4.8.1
39
39
  Requires-Dist: click==8.1.7
40
- Requires-Dist: esphome-dashboard==20250212.0
41
- Requires-Dist: aioesphomeapi==29.9.0
42
- Requires-Dist: zeroconf==0.146.3
40
+ Requires-Dist: esphome-dashboard==20250415.0
41
+ Requires-Dist: aioesphomeapi==29.10.0
42
+ Requires-Dist: zeroconf==0.146.5
43
43
  Requires-Dist: puremagic==1.28
44
44
  Requires-Dist: ruamel.yaml==0.18.10
45
45
  Requires-Dist: esphome-glyphsets==0.2.0