Merge branch 'mdns_cache' into integration

This commit is contained in:
J. Nick Koston 2023-11-09 23:19:25 -06:00
commit 4b50b6cfc7
No known key found for this signature in database
12 changed files with 138 additions and 53 deletions

View file

@ -3,7 +3,7 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/psf/black-pre-commit-mirror - repo: https://github.com/psf/black-pre-commit-mirror
rev: 23.10.1 rev: 23.11.0
hooks: hooks:
- id: black - id: black
args: args:

View file

@ -246,6 +246,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet esphome/components/rc522_spi/* @glmnet
esphome/components/resistance_sampler/* @jesserockz
esphome/components/restart/* @esphome/core esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz esphome/components/rf_bridge/* @jesserockz
esphome/components/rgbct/* @jesserockz esphome/components/rgbct/* @jesserockz

View file

@ -2,7 +2,7 @@ from math import log
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import sensor from esphome.components import sensor, resistance_sampler
from esphome.const import ( from esphome.const import (
CONF_CALIBRATION, CONF_CALIBRATION,
CONF_REFERENCE_RESISTANCE, CONF_REFERENCE_RESISTANCE,
@ -15,6 +15,8 @@ from esphome.const import (
UNIT_CELSIUS, UNIT_CELSIUS,
) )
AUTO_LOAD = ["resistance_sampler"]
ntc_ns = cg.esphome_ns.namespace("ntc") ntc_ns = cg.esphome_ns.namespace("ntc")
NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor) NTC = ntc_ns.class_("NTC", cg.Component, sensor.Sensor)
@ -124,7 +126,7 @@ CONFIG_SCHEMA = (
) )
.extend( .extend(
{ {
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR): cv.use_id(resistance_sampler.ResistanceSampler),
cv.Required(CONF_CALIBRATION): process_calibration, cv.Required(CONF_CALIBRATION): process_calibration,
} }
) )

View file

@ -1,7 +1,8 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/components/resistance_sampler/resistance_sampler.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome { namespace esphome {
namespace resistance { namespace resistance {
@ -11,7 +12,7 @@ enum ResistanceConfiguration {
DOWNSTREAM, DOWNSTREAM,
}; };
class ResistanceSensor : public Component, public sensor::Sensor { class ResistanceSensor : public Component, public sensor::Sensor, resistance_sampler::ResistanceSampler {
public: public:
void set_sensor(Sensor *sensor) { sensor_ = sensor; } void set_sensor(Sensor *sensor) { sensor_ = sensor; }
void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; } void set_configuration(ResistanceConfiguration configuration) { configuration_ = configuration; }

View file

@ -1,6 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.components import sensor from esphome.components import sensor, resistance_sampler
from esphome.const import ( from esphome.const import (
CONF_SENSOR, CONF_SENSOR,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
@ -8,8 +8,15 @@ from esphome.const import (
ICON_FLASH, ICON_FLASH,
) )
AUTO_LOAD = ["resistance_sampler"]
resistance_ns = cg.esphome_ns.namespace("resistance") resistance_ns = cg.esphome_ns.namespace("resistance")
ResistanceSensor = resistance_ns.class_("ResistanceSensor", cg.Component, sensor.Sensor) ResistanceSensor = resistance_ns.class_(
"ResistanceSensor",
cg.Component,
sensor.Sensor,
resistance_sampler.ResistanceSampler,
)
CONF_REFERENCE_VOLTAGE = "reference_voltage" CONF_REFERENCE_VOLTAGE = "reference_voltage"
CONF_CONFIGURATION = "configuration" CONF_CONFIGURATION = "configuration"

View file

@ -0,0 +1,6 @@
import esphome.codegen as cg
resistance_sampler_ns = cg.esphome_ns.namespace("resistance_sampler")
ResistanceSampler = resistance_sampler_ns.class_("ResistanceSampler")
CODEOWNERS = ["@jesserockz"]

View file

@ -0,0 +1,10 @@
#pragma once
namespace esphome {
namespace resistance_sampler {
/// Abstract interface to mark components that provide resistance values.
class ResistanceSampler {};
} // namespace resistance_sampler
} // namespace esphome

View file

@ -43,11 +43,17 @@ def validate_mode(mode):
return mode return mode
def validate_pin(pin):
if pin in (8, 9):
raise cv.Invalid(f"pin {pin} doesn't exist")
return pin
XL9535_PIN_SCHEMA = cv.All( XL9535_PIN_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(XL9535GPIOPin), cv.GenerateID(): cv.declare_id(XL9535GPIOPin),
cv.Required(CONF_XL9535): cv.use_id(XL9535Component), cv.Required(CONF_XL9535): cv.use_id(XL9535Component),
cv.Required(CONF_NUMBER): cv.int_range(min=0, max=15), cv.Required(CONF_NUMBER): cv.All(cv.int_range(min=0, max=17), validate_pin),
cv.Optional(CONF_MODE, default={}): cv.All( cv.Optional(CONF_MODE, default={}): cv.All(
{ {
cv.Optional(CONF_INPUT, default=False): cv.boolean, cv.Optional(CONF_INPUT, default=False): cv.boolean,

View file

@ -18,6 +18,7 @@ import shutil
import subprocess import subprocess
import threading import threading
from pathlib import Path from pathlib import Path
from typing import Any
import tornado import tornado
import tornado.concurrent import tornado.concurrent
@ -398,19 +399,40 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
raise NotImplementedError raise NotImplementedError
class EsphomeLogsHandler(EsphomeCommandWebSocket): DASHBOARD_COMMAND = ["esphome", "--dashboard"]
def build_command(self, json_message):
config_file = settings.rel_path(json_message["configuration"])
class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
"""Base class for commands that require a port."""
def run_command(self, args: list[str], json_message: dict[str, Any]) -> list[str]:
"""Build the command to run."""
configuration = json_message["configuration"]
config_file = settings.rel_path(configuration)
port = json_message["port"]
if (
port == "OTA"
and (mdns := MDNS_CONTAINER.get_mdns())
and (host_name := mdns.filename_to_host_name_thread_safe(configuration))
and (address := mdns.resolve_host_thread_safe(host_name))
):
port = address
return [ return [
"esphome", *DASHBOARD_COMMAND,
"--dashboard", *args,
"logs",
config_file, config_file,
"--device", "--device",
json_message["port"], port,
] ]
class EsphomeLogsHandler(EsphomePortCommandWebSocket):
def build_command(self, json_message: dict[str, Any]) -> list[str]:
"""Build the command to run."""
return self.run_command(["logs"], json_message)
class EsphomeRenameHandler(EsphomeCommandWebSocket): class EsphomeRenameHandler(EsphomeCommandWebSocket):
old_name: str old_name: str
@ -418,8 +440,7 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket):
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
self.old_name = json_message["configuration"] self.old_name = json_message["configuration"]
return [ return [
"esphome", *DASHBOARD_COMMAND,
"--dashboard",
"rename", "rename",
config_file, config_file,
json_message["newName"], json_message["newName"],
@ -435,36 +456,22 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket):
PING_RESULT.pop(self.old_name, None) PING_RESULT.pop(self.old_name, None)
class EsphomeUploadHandler(EsphomeCommandWebSocket): class EsphomeUploadHandler(EsphomePortCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) """Build the command to run."""
return [ return self.run_command(["upload"], json_message)
"esphome",
"--dashboard",
"upload",
config_file,
"--device",
json_message["port"],
]
class EsphomeRunHandler(EsphomeCommandWebSocket): class EsphomeRunHandler(EsphomePortCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"]) """Build the command to run."""
return [ return self.run_command(["run"], json_message)
"esphome",
"--dashboard",
"run",
config_file,
"--device",
json_message["port"],
]
class EsphomeCompileHandler(EsphomeCommandWebSocket): class EsphomeCompileHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message):
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
command = ["esphome", "--dashboard", "compile"] command = [*DASHBOARD_COMMAND, "compile"]
if json_message.get("only_generate", False): if json_message.get("only_generate", False):
command.append("--only-generate") command.append("--only-generate")
command.append(config_file) command.append(config_file)
@ -474,7 +481,7 @@ class EsphomeCompileHandler(EsphomeCommandWebSocket):
class EsphomeValidateHandler(EsphomeCommandWebSocket): class EsphomeValidateHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message):
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
command = ["esphome", "--dashboard", "config", config_file] command = [*DASHBOARD_COMMAND, "config", config_file]
if not settings.streamer_mode: if not settings.streamer_mode:
command.append("--show-secrets") command.append("--show-secrets")
return command return command
@ -483,28 +490,28 @@ class EsphomeValidateHandler(EsphomeCommandWebSocket):
class EsphomeCleanMqttHandler(EsphomeCommandWebSocket): class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message):
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
return ["esphome", "--dashboard", "clean-mqtt", config_file] return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
class EsphomeCleanHandler(EsphomeCommandWebSocket): class EsphomeCleanHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message):
config_file = settings.rel_path(json_message["configuration"]) config_file = settings.rel_path(json_message["configuration"])
return ["esphome", "--dashboard", "clean", config_file] return [*DASHBOARD_COMMAND, "clean", config_file]
class EsphomeVscodeHandler(EsphomeCommandWebSocket): class EsphomeVscodeHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message):
return ["esphome", "--dashboard", "-q", "vscode", "dummy"] return [*DASHBOARD_COMMAND, "-q", "vscode", "dummy"]
class EsphomeAceEditorHandler(EsphomeCommandWebSocket): class EsphomeAceEditorHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message):
return ["esphome", "--dashboard", "-q", "vscode", "--ace", settings.config_dir] return [*DASHBOARD_COMMAND, "-q", "vscode", "--ace", settings.config_dir]
class EsphomeUpdateAllHandler(EsphomeCommandWebSocket): class EsphomeUpdateAllHandler(EsphomeCommandWebSocket):
def build_command(self, json_message): def build_command(self, json_message):
return ["esphome", "--dashboard", "update-all", settings.config_dir] return [*DASHBOARD_COMMAND, "update-all", settings.config_dir]
class SerialPortRequestHandler(BaseHandler): class SerialPortRequestHandler(BaseHandler):
@ -964,23 +971,38 @@ class BoardsRequestHandler(BaseHandler):
class MDNSStatusThread(threading.Thread): class MDNSStatusThread(threading.Thread):
def __init__(self): """Thread that updates the mdns status."""
def __init__(self) -> None:
"""Initialize the MDNSStatusThread.""" """Initialize the MDNSStatusThread."""
super().__init__() super().__init__()
self.zeroconf: EsphomeZeroconf | None = None
# This is the current mdns state for each host (True, False, None) # This is the current mdns state for each host (True, False, None)
self.host_mdns_state: dict[str, bool | None] = {} self.host_mdns_state: dict[str, bool | None] = {}
# This is the hostnames to filenames mapping # This is the hostnames to filenames mapping
self.host_name_to_filename: dict[str, str] = {} self.host_name_to_filename: dict[str, str] = {}
self.filename_to_host_name: dict[str, str] = {}
# This is a set of host names to track (i.e no_mdns = false) # This is a set of host names to track (i.e no_mdns = false)
self.host_name_with_mdns_enabled: set[set] = set() self.host_name_with_mdns_enabled: set[set] = set()
self._refresh_hosts() self._refresh_hosts()
def filename_to_host_name_thread_safe(self, filename: str) -> str | None:
"""Resolve a filename to an address in a thread-safe manner."""
return self.filename_to_host_name.get(filename)
def resolve_host_thread_safe(self, host_name: str) -> str | None:
"""Resolve a host name to an address in a thread-safe manner."""
if zc := self.zeroconf:
return zc.resolve_host(host_name)
return None
def _refresh_hosts(self): def _refresh_hosts(self):
"""Refresh the hosts to track.""" """Refresh the hosts to track."""
entries = _list_dashboard_entries() entries = _list_dashboard_entries()
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
host_mdns_state = self.host_mdns_state host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename host_name_to_filename = self.host_name_to_filename
filename_to_host_name = self.filename_to_host_name
for entry in entries: for entry in entries:
name = entry.name name = entry.name
@ -1003,11 +1025,13 @@ class MDNSStatusThread(threading.Thread):
# so when we get an mdns update we can map it back # so when we get an mdns update we can map it back
# to the filename # to the filename
host_name_to_filename[name] = filename host_name_to_filename[name] = filename
filename_to_host_name[filename] = name
def run(self): def run(self):
global IMPORT_RESULT global IMPORT_RESULT
zc = EsphomeZeroconf() zc = EsphomeZeroconf()
self.zeroconf = zc
host_mdns_state = self.host_mdns_state host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename host_name_to_filename = self.host_name_to_filename
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
@ -1035,6 +1059,7 @@ class MDNSStatusThread(threading.Thread):
browser.cancel() browser.cancel()
zc.close() zc.close()
self.zeroconf = None
class PingStatusThread(threading.Thread): class PingStatusThread(threading.Thread):
@ -1211,11 +1236,26 @@ class UndoDeleteRequestHandler(BaseHandler):
shutil.move(os.path.join(trash_path, configuration), config_file) shutil.move(os.path.join(trash_path, configuration), config_file)
class MDNSContainer:
def __init__(self) -> None:
"""Initialize the MDNSContainer."""
self._mdns: MDNSStatusThread | None = None
def set_mdns(self, mdns: MDNSStatusThread) -> None:
"""Set the MDNSStatusThread instance."""
self._mdns = mdns
def get_mdns(self) -> MDNSStatusThread | None:
"""Return the MDNSStatusThread instance."""
return self._mdns
PING_RESULT: dict = {} PING_RESULT: dict = {}
IMPORT_RESULT = {} IMPORT_RESULT = {}
STOP_EVENT = threading.Event() STOP_EVENT = threading.Event()
PING_REQUEST = threading.Event() PING_REQUEST = threading.Event()
MQTT_PING_REQUEST = threading.Event() MQTT_PING_REQUEST = threading.Event()
MDNS_CONTAINER = MDNSContainer()
class LoginHandler(BaseHandler): class LoginHandler(BaseHandler):
@ -1506,6 +1546,7 @@ def start_web_server(args):
status_thread = PingStatusThread() status_thread = PingStatusThread()
else: else:
status_thread = MDNSStatusThread() status_thread = MDNSStatusThread()
MDNS_CONTAINER.set_mdns(status_thread)
status_thread.start() status_thread.start()
if settings.status_use_mqtt: if settings.status_use_mqtt:
@ -1519,6 +1560,7 @@ def start_web_server(args):
STOP_EVENT.set() STOP_EVENT.set()
PING_REQUEST.set() PING_REQUEST.set()
status_thread.join() status_thread.join()
MDNS_CONTAINER.set_mdns(None)
if settings.status_use_mqtt: if settings.status_use_mqtt:
status_thread_mqtt.join() status_thread_mqtt.join()
MQTT_PING_REQUEST.set() MQTT_PING_REQUEST.set()

View file

@ -147,12 +147,13 @@ class DashboardImportDiscovery:
class EsphomeZeroconf(Zeroconf): class EsphomeZeroconf(Zeroconf):
def resolve_host(self, host: str, timeout=3.0): def resolve_host(self, host: str, timeout: float = 3.0) -> str | None:
"""Resolve a host name to an IP address.""" """Resolve a host name to an IP address."""
name = host.partition(".")[0] name = host.partition(".")[0]
info = HostResolver(f"{name}.{ESPHOME_SERVICE_TYPE}", ESPHOME_SERVICE_TYPE) info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}")
if (info.load_from_cache(self) or info.request(self, timeout * 1000)) and ( if (
addresses := info.ip_addresses_by_version(IPVersion.V4Only) info.load_from_cache(self)
): or (timeout and info.request(self, timeout * 1000))
) and (addresses := info.ip_addresses_by_version(IPVersion.V4Only)):
return str(addresses[0]) return str(addresses[0])
return None return None

View file

@ -1,6 +1,6 @@
pylint==2.17.6 pylint==2.17.6
flake8==6.1.0 # also change in .pre-commit-config.yaml when updating flake8==6.1.0 # also change in .pre-commit-config.yaml when updating
black==23.10.1 # also change in .pre-commit-config.yaml when updating black==23.11.0 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating
pre-commit pre-commit

View file

@ -425,6 +425,15 @@ binary_sensor:
input: true input: true
inverted: false inverted: false
- platform: gpio
name: XL9535 Pin 17
pin:
xl9535: xl9535_hub
number: 17
mode:
input: true
inverted: false
climate: climate:
- platform: tuya - platform: tuya
id: tuya_climate id: tuya_climate