This commit is contained in:
Jimmy Hedman 2024-05-02 15:45:58 +12:00 committed by GitHub
commit 8a0ff5d591
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 377 additions and 2 deletions

View File

@ -3,6 +3,7 @@
#include <string>
#include <vector>
#include "esphome/core/component.h"
#include "esphome/components/network/ip_address.h"
namespace esphome {
namespace mdns {
@ -35,6 +36,8 @@ class MDNSComponent : public Component {
void add_extra_service(MDNSService service) { services_extra_.push_back(std::move(service)); }
std::vector<network::IPAddress> resolve(const std::string &servicename);
void on_shutdown() override;
protected:
@ -44,5 +47,7 @@ class MDNSComponent : public Component {
void compile_records_();
};
extern MDNSComponent *global_mdns; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace mdns
} // namespace esphome

View File

@ -12,6 +12,7 @@ namespace mdns {
static const char *const TAG = "mdns";
void MDNSComponent::setup() {
global_mdns = this;
this->compile_records_();
esp_err_t err = mdns_init();
@ -48,11 +49,43 @@ void MDNSComponent::setup() {
}
}
std::vector<network::IPAddress> MDNSComponent::resolve(const std::string &servicename) {
std::vector<network::IPAddress> resolved;
mdns_result_t *results = nullptr;
mdns_ip_addr_t *a = nullptr;
esp_err_t err = mdns_query_ptr(("_" + servicename).c_str(), "_tcp", 3000, 20, &results);
if (err) {
ESP_LOGE(TAG, "Query Failed: %s", esp_err_to_name(err));
return {};
}
if (!results) {
ESP_LOGW(TAG, "No results found!");
return {};
}
while (results) {
a = results->addr;
while (a) {
network::IPAddress ip_addr = network::IPAddress(&a->addr);
if (std::count(resolved.begin(), resolved.end(), ip_addr) == 0) {
resolved.push_back(ip_addr);
}
ESP_LOGVV(TAG, "Found mDNS %s", ip_addr.str().c_str());
a = a->next;
}
results = results->next;
}
mdns_query_results_free(results);
return resolved;
}
void MDNSComponent::on_shutdown() {
mdns_free();
delay(40); // Allow the mdns packets announcing service removal to be sent
}
MDNSComponent *global_mdns = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace mdns
} // namespace esphome

View File

@ -10,7 +10,10 @@
namespace esphome {
namespace mdns {
static const char *const TAG = "mdns";
void MDNSComponent::setup() {
global_mdns = this;
this->compile_records_();
MDNS.begin(this->hostname_.c_str());
@ -34,6 +37,18 @@ void MDNSComponent::setup() {
}
}
}
std::vector<network::IPAddress> MDNSComponent::resolve(const std::string &servicename) {
std::vector<network::IPAddress> resolved;
uint8_t n = MDNS.queryService(servicename.c_str(), "tcp");
for (uint8_t i = 0; i < n; i++) {
network::IPAddress ip_addr = network::IPAddress(MDNS.IP(i));
if (std::count(resolved.begin(), resolved.end(), ip_addr) == 0) {
resolved.push_back(ip_addr);
}
ESP_LOGVV(TAG, "Found mDNS %s", ip_addr.str().c_str());
}
return resolved;
}
void MDNSComponent::loop() { MDNS.update(); }
@ -42,6 +57,8 @@ void MDNSComponent::on_shutdown() {
delay(10);
}
MDNSComponent *global_mdns = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace mdns
} // namespace esphome

View File

@ -11,6 +11,7 @@ namespace esphome {
namespace mdns {
void MDNSComponent::setup() {
global_mdns = this;
this->compile_records_();
MDNS.begin(this->hostname_.c_str());
@ -35,8 +36,12 @@ void MDNSComponent::setup() {
}
}
// Libre tiny doesn't have a "full" mDNS implementation
std::vector<network::IPAddress> MDNSComponent::resolve(const std::string &servicename) { return {}; }
void MDNSComponent::on_shutdown() {}
MDNSComponent *global_mdns = nullptr;
} // namespace mdns
} // namespace esphome

View File

@ -10,7 +10,10 @@
namespace esphome {
namespace mdns {
static const char *const TAG = "mdns";
void MDNSComponent::setup() {
global_mdns = this;
this->compile_records_();
MDNS.begin(this->hostname_.c_str());
@ -35,6 +38,19 @@ void MDNSComponent::setup() {
}
}
std::vector<network::IPAddress> MDNSComponent::resolve(const std::string &servicename) {
std::vector<network::IPAddress> resolved;
uint8_t n = MDNS.queryService(servicename.c_str(), "tcp");
for (uint8_t i = 0; i < n; i++) {
network::IPAddress ip_addr_ = network::IPAddress(MDNS.IP(i));
if (std::count(resolved.begin(), resolved.end(), ip_addr_) == 0) {
resolved.push_back(ip_addr_);
}
ESP_LOGVV(TAG, "Found mDNS %s", ip_addr_.str().c_str());
}
return resolved;
}
void MDNSComponent::loop() { MDNS.update(); }
void MDNSComponent::on_shutdown() {
@ -42,6 +58,7 @@ void MDNSComponent::on_shutdown() {
delay(40);
}
MDNSComponent *global_mdns = nullptr;
} // namespace mdns
} // namespace esphome

View File

@ -1,21 +1,29 @@
from esphome.core import CORE
from __future__ import annotations
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import helpers
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.const import (
CONF_ENABLE_IPV6,
CONF_MIN_IPV6_ADDR_COUNT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
CONF_HOSTS,
CONF_HOSTSFILE,
CONF_IP_ADDRESS,
CONF_NAME,
)
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["mdns"]
network_ns = cg.esphome_ns.namespace("network")
IPAddress = network_ns.class_("IPAddress")
Resolver = network_ns.class_("Resolver")
CONF_NETWORK_ID = "network_id"
CONFIG_SCHEMA = cv.Schema(
{
@ -23,10 +31,34 @@ CONFIG_SCHEMA = cv.Schema(
cv.boolean, cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040])
),
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
cv.GenerateID(CONF_NETWORK_ID): cv.declare_id(Resolver),
cv.Optional(CONF_HOSTSFILE): cv.file_,
cv.Optional(CONF_HOSTS): cv.ensure_list(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string,
cv.Required(CONF_IP_ADDRESS): cv.string,
}
)
),
}
)
def parse_hosts_file(hosts_contents: str) -> list[tuple[str, IPAddress]]:
"""Parse a hosts file"""
hosts: list[tuple[str, IPAddress]] = []
for line in hosts_contents.splitlines():
if line.startswith("#"):
continue
split_line = line.split()
if len(split_line) < 2:
continue
ip_address: IPAddress = IPAddress(split_line[0])
hosts.extend([(host, ip_address) for host in split_line[1:]])
return hosts
async def to_code(config):
if CONF_ENABLE_IPV6 in config:
cg.add_define("USE_NETWORK_IPV6", config[CONF_ENABLE_IPV6])
@ -46,3 +78,17 @@ async def to_code(config):
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
if CORE.is_esp8266:
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
hosts = []
if CONF_HOSTS in config:
hosts = [
(host[CONF_NAME], IPAddress(host[CONF_IP_ADDRESS]))
for host in config[CONF_HOSTS]
]
if CONF_HOSTSFILE in config:
hosts_contents = helpers.read_file(
CORE.relative_config_path(config[CONF_HOSTSFILE])
)
hosts.extend(parse_hosts_file(hosts_contents))
map_ = cg.std_ns.class_("multimap").template(cg.std_string, IPAddress)
cg.new_Pvariable(config[CONF_NETWORK_ID], map_(hosts))

View File

@ -0,0 +1,99 @@
#include <vector>
#include "resolver.h"
#include "lwip/dns.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/hal.h"
#include "esphome/core/defines.h"
#ifdef USE_MDNS
#include "esphome/components/mdns/mdns_component.h"
#endif
#ifdef ENABLE_IPV6
#define ESPHOME_DNS_ADDRTYPE LWIP_DNS_ADDRTYPE_IPV6_IPV4
#else
#define ESPHOME_DNS_ADDRTYPE LWIP_DNS_ADDRTYPE_IPV4
#endif
namespace esphome {
namespace network {
static const char *const TAG = "resolver";
Resolver::Resolver() { global_resolver = this; }
Resolver::Resolver(std::multimap<std::string, network::IPAddress> hosts) : hosts_(std::move(hosts)) {
global_resolver = this;
}
// TODO(HeMan): resolve needs to return multiple IP addresses
std::vector<network::IPAddress> Resolver::resolve(const std::string &hostname) {
if (this->hosts_.count(hostname) > 0) {
std::vector<network::IPAddress> resolved;
for (auto a = hosts_.find(hostname); a != hosts_.end(); a++) {
resolved.push_back(a->second);
ESP_LOGVV(TAG, "Found %s in hosts section", hostname.c_str());
}
return resolved;
}
#ifdef USE_MDNS
ESP_LOGV(TAG, "Looking for %s with mDNS", hostname.c_str());
std::vector<network::IPAddress> resolved_mdns = mdns::global_mdns->resolve(hostname);
if (!resolved_mdns.empty()) {
ESP_LOGVV(TAG, "Found %s in mDNS", hostname.c_str());
return resolved_mdns;
}
#endif
ip_addr_t addr;
ESP_LOGVV(TAG, "Resolving %s", hostname.c_str());
err_t err =
dns_gethostbyname_addrtype(hostname.c_str(), &addr, Resolver::dns_found_callback, this, ESPHOME_DNS_ADDRTYPE);
if (err == ERR_OK) {
return {network::IPAddress(&addr)};
}
this->connect_begin_ = millis();
while (!this->dns_resolved_ && !this->dns_resolve_error_ && (millis() - this->connect_begin_ < 2000)) {
switch (err) {
case ERR_OK: {
// Got IP immediately
ESP_LOGVV(TAG, "Found %s in DNS", hostname.c_str());
this->dns_resolved_ = true;
this->ip_ = network::IPAddress(&addr);
return {this->ip_};
}
case ERR_INPROGRESS: {
// wait for callback
ESP_LOGVV(TAG, "Resolving IP address...");
break;
}
default:
case ERR_ARG: {
// error
ESP_LOGW(TAG, "Error resolving IP address: %d", err);
break;
}
}
delay_microseconds_safe(100);
}
if (this->dns_resolve_error_) {
ESP_LOGV(TAG, "Error resolving IP address");
}
if (!this->dns_resolved_) {
ESP_LOGVV(TAG, "Not resolved");
}
return {this->ip_};
}
void Resolver::dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg) {
auto *a_this = (Resolver *) callback_arg;
if (ipaddr == nullptr) {
a_this->dns_resolve_error_ = true;
} else {
ESP_LOGVV(TAG, "Found %s in DNS", name);
a_this->ip_ = network::IPAddress(ipaddr);
a_this->dns_resolved_ = true;
}
}
Resolver *global_resolver = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace network
} // namespace esphome

View File

@ -0,0 +1,29 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include "ip_address.h"
namespace esphome {
namespace network {
class Resolver {
public:
Resolver();
Resolver(std::multimap<std::string, network::IPAddress> hosts);
~Resolver();
std::vector<network::IPAddress> resolve(const std::string &hostname);
protected:
static void dns_found_callback(const char *name, const ip_addr_t *ipaddr, void *callback_arg);
std::multimap<std::string, network::IPAddress> hosts_;
network::IPAddress ip_;
bool dns_resolved_{false};
bool dns_resolve_error_{false};
uint32_t connect_begin_;
};
extern Resolver *global_resolver; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace network
} // namespace esphome

View File

@ -1,3 +1,4 @@
#include <map>
#include "util.h"
#include "esphome/core/defines.h"

View File

@ -36,6 +36,7 @@ extern "C" {
#include "esphome/core/hal.h"
#include "esphome/core/util.h"
#include "esphome/core/application.h"
#include "esphome/components/network/util.h"
namespace esphome {
namespace wifi {

View File

@ -335,6 +335,8 @@ CONF_HIDDEN = "hidden"
CONF_HIDE_TIMESTAMP = "hide_timestamp"
CONF_HIGH = "high"
CONF_HIGH_VOLTAGE_REFERENCE = "high_voltage_reference"
CONF_HOSTS = "hosts"
CONF_HOSTSFILE = "hostsfile"
CONF_HOUR = "hour"
CONF_HOURS = "hours"
CONF_HUMIDITY = "humidity"

9
tests/hosts Normal file
View File

@ -0,0 +1,9 @@
# IPv4 test
192.168.1.1 other.example.com
# IPv6 test
2001:db8:4455:6677::35 other6.example.com
# Empty line test
# multi name test
192.168.1.10 multi1.example.com multi1

View File

@ -80,6 +80,14 @@ wifi:
network:
enable_ipv6: true
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
hostsfile: hosts
mdns:
disabled: false

View File

@ -36,6 +36,14 @@ ethernet:
network:
enable_ipv6: true
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
hostsfile: hosts
mdns:
disabled: true

View File

@ -29,6 +29,14 @@ ethernet:
network:
enable_ipv6: true
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
hostsfile: hosts
mqtt:
broker: test.mosquitto.org

View File

@ -24,6 +24,13 @@ wifi:
network:
enable_ipv6: true
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
api:

View File

@ -18,6 +18,14 @@ wifi:
network:
enable_ipv6: true
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
hostsfile: hosts
api:

View File

@ -5,6 +5,14 @@ wifi:
network:
enable_ipv6: true
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
hostsfile: hosts
esp32:
board: lolin_c3_mini

View File

@ -9,6 +9,16 @@ rtl87xx:
esphome:
name: rtl87xx-test
network:
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
hostsfile: hosts
logger:
ota:

View File

@ -9,6 +9,16 @@ bk72xx:
esphome:
name: bk72xx-test
network:
hosts:
- name: www.example.com
ip_address: 192.168.1.80
- name: www.mqtt.com
ip_address: 10.20.18.33
- name: ipv6.example.com
ip_address: 2001:db8:4455:6677::32
hostsfile: hosts
logger:
ota:

View File

@ -0,0 +1,44 @@
from __future__ import annotations
from typing import Any
import pytest
from esphome.components import network
from esphome.cpp_generator import MockObj
def _convert_mockobjs_to_string_literal(obj: Any) -> Any:
"""Convert mock objects to their base objects."""
if isinstance(obj, MockObj):
return str(obj.base.args.args[0].string)
if isinstance(obj, list):
return [_convert_mockobjs_to_string_literal(x) for x in obj]
if isinstance(obj, tuple):
return tuple(_convert_mockobjs_to_string_literal(x) for x in obj)
if isinstance(obj, dict):
return {k: _convert_mockobjs_to_string_literal(v) for k, v in obj.items()}
return obj
@pytest.mark.parametrize(
("contents", "expected"),
(
("127.0.0.1 localhost", [("localhost", "127.0.0.1")]),
(":1 localhost", [("localhost", ":1")]),
("#192.168.1.1 commented", []),
(
"192.168.1.10 multi1.example.com multi1\n:1 localhost",
[
("multi1.example.com", "192.168.1.10"),
("multi1", "192.168.1.10"),
("localhost", ":1"),
],
),
),
)
def test_parse_hosts_file(contents: str, expected: list[tuple[str, str]]):
"""Test parsing a hosts file."""
actual = network.parse_hosts_file(contents)
unmocked_actual = _convert_mockobjs_to_string_literal(actual)
assert _convert_mockobjs_to_string_literal(unmocked_actual) == expected