From c301ae3645fec945ef907be1314198f2e6411bd9 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 17 Jan 2023 10:28:09 +1300 Subject: [PATCH] Add friendly_name to device (#4296) --- esphome/components/api/api.proto | 2 ++ esphome/components/api/api_connection.cpp | 1 + esphome/components/api/api_pb2.cpp | 9 ++++++ esphome/components/api/api_pb2.h | 1 + .../components/dashboard_import/__init__.py | 27 +++++++++++------ esphome/components/mdns/mdns_component.cpp | 3 ++ esphome/components/web_server/web_server.cpp | 2 +- esphome/const.py | 1 + esphome/core/__init__.py | 3 ++ esphome/core/application.h | 13 +++++++- esphome/core/config.py | 4 +++ esphome/dashboard/dashboard.py | 30 +++++++++++++++++-- esphome/dashboard/util.py | 23 ++++++++++++++ esphome/storage_json.py | 12 +++++++- esphome/wizard.py | 14 +++++++-- esphome/zeroconf.py | 6 ++++ requirements.txt | 2 +- tests/dummy_main.cpp | 2 +- 18 files changed, 137 insertions(+), 18 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index e1bc7b0a57..013bb123f9 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -206,6 +206,8 @@ message DeviceInfoResponse { uint32 bluetooth_proxy_version = 11; string manufacturer = 12; + + string friendly_name = 13; } message ListEntitiesRequest { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index aac58587d1..65659941d6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -930,6 +930,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { DeviceInfoResponse resp{}; resp.uses_password = this->parent_->uses_password(); resp.name = App.get_name(); + resp.friendly_name = App.get_friendly_name(); resp.mac_address = get_mac_address_pretty(); resp.esphome_version = ESPHOME_VERSION; resp.compilation_time = App.get_compilation_time(); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f108d38e8f..67c011e76c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -628,6 +628,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v this->manufacturer = value.as_string(); return true; } + case 13: { + this->friendly_name = value.as_string(); + return true; + } default: return false; } @@ -645,6 +649,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(10, this->webserver_port); buffer.encode_uint32(11, this->bluetooth_proxy_version); buffer.encode_string(12, this->manufacturer); + buffer.encode_string(13, this->friendly_name); } #ifdef HAS_PROTO_MESSAGE_DUMP void DeviceInfoResponse::dump_to(std::string &out) const { @@ -699,6 +704,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { out.append(" manufacturer: "); out.append("'").append(this->manufacturer).append("'"); out.append("\n"); + + out.append(" friendly_name: "); + out.append("'").append(this->friendly_name).append("'"); + out.append("\n"); out.append("}"); } #endif diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8a78f1ad03..3b804f1de7 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -276,6 +276,7 @@ class DeviceInfoResponse : public ProtoMessage { uint32_t webserver_port{0}; uint32_t bluetooth_proxy_version{0}; std::string manufacturer{}; + std::string friendly_name{}; void encode(ProtoWriteBuffer buffer) const override; #ifdef HAS_PROTO_MESSAGE_DUMP void dump_to(std::string &out) const override; diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 0eb65579f9..d377c257a7 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -1,14 +1,15 @@ from pathlib import Path +from typing import Optional + import requests import esphome.codegen as cg import esphome.config_validation as cv +from esphome import git from esphome.components.packages import validate_source_shorthand -from esphome.const import CONF_WIFI, CONF_REF +from esphome.const import CONF_REF, CONF_WIFI from esphome.wizard import wizard_file from esphome.yaml_util import dump -from esphome import git - dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import") @@ -66,7 +67,12 @@ async def to_code(config): def import_config( - path: str, name: str, project_name: str, import_url: str, network: str = CONF_WIFI + path: str, + name: str, + friendly_name: Optional[str], + project_name: str, + import_url: str, + network: str = CONF_WIFI, ) -> None: p = Path(path) @@ -77,6 +83,7 @@ def import_config( p.write_text( wizard_file( name=name, + friendly_name=friendly_name, platform="ESP32" if "esp32" in import_url else "ESP8266", board="esp32dev" if "esp32" in import_url else "esp01_1m", ssid="!secret wifi_ssid", @@ -98,13 +105,15 @@ def import_config( p.write_text(req.text, encoding="utf8") else: + substitutions = {"name": name} + esphome_core = {"name": "${name}", "name_add_mac_suffix": False} + if friendly_name: + substitutions["friendly_name"] = friendly_name + esphome_core["friendly_name"] = "${friendly_name}" config = { - "substitutions": {"name": name}, + "substitutions": substitutions, "packages": {project_name: import_url}, - "esphome": { - "name": "${name}", - "name_add_mac_suffix": False, - }, + "esphome": esphome_core, } output = dump(config) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index a3f38322b3..db47c8823c 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -30,6 +30,9 @@ void MDNSComponent::compile_records_() { service.service_type = "_esphomelib"; service.proto = "_tcp"; service.port = api::global_api_server->get_port(); + if (App.get_friendly_name().empty()) { + service.txt_records.push_back({"friendly_name", App.get_friendly_name()}); + } service.txt_records.push_back({"version", ESPHOME_VERSION}); service.txt_records.push_back({"mac", get_mac_address()}); const char *platform = nullptr; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1f043b43fc..ad9ced0614 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -104,7 +104,7 @@ void WebServer::setup() { // Configure reconnect timeout and send config client->send(json::build_json([this](JsonObject root) { - root["title"] = App.get_name(); + root["title"] = App.get_friendly_name().empty() ? App.get_name() : App.get_friendly_name(); root["ota"] = this->allow_ota_; root["lang"] = "en"; }).c_str(), diff --git a/esphome/const.py b/esphome/const.py index b98126ed34..3799e55e35 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -260,6 +260,7 @@ CONF_FRAGMENTATION = "fragmentation" CONF_FRAMEWORK = "framework" CONF_FREE = "free" CONF_FREQUENCY = "frequency" +CONF_FRIENDLY_NAME = "friendly_name" CONF_FROM = "from" CONF_FULL_SPECTRUM = "full_spectrum" CONF_FULL_UPDATE_EVERY = "full_update_every" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 09115c2791..545fae381f 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -453,6 +453,8 @@ class EsphomeCore: self.ace = False # The name of the node self.name: Optional[str] = None + # The friendly name of the node + self.friendly_name: Optional[str] = None # Additional data components can store temporary data in # The first key to this dict should always be the integration name self.data = {} @@ -492,6 +494,7 @@ class EsphomeCore: def reset(self): self.dashboard = False self.name = None + self.friendly_name = None self.data = {} self.config_path = None self.build_path = None diff --git a/esphome/core/application.h b/esphome/core/application.h index 6376987f66..eaae34635f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -53,13 +53,20 @@ namespace esphome { class Application { public: - void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { + void pre_setup(const std::string &name, const std::string &friendly_name, const char *compilation_time, + bool name_add_mac_suffix) { arch_init(); this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { this->name_ = name + "-" + get_mac_address().substr(6); + if (friendly_name.empty()) { + this->friendly_name_ = ""; + } else { + this->friendly_name_ = friendly_name + " " + get_mac_address().substr(6); + } } else { this->name_ = name; + this->friendly_name_ = friendly_name; } this->compilation_time_ = compilation_time; } @@ -134,6 +141,9 @@ class Application { /// Get the name of this Application set by set_name(). const std::string &get_name() const { return this->name_; } + /// Get the friendly name of this Application set by set_friendly_name(). + const std::string &get_friendly_name() const { return this->friendly_name_; } + bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } const std::string &get_compilation_time() const { return this->compilation_time_; } @@ -338,6 +348,7 @@ class Application { #endif std::string name_; + std::string friendly_name_; std::string compilation_time_; bool name_add_mac_suffix_; uint32_t last_loop_{0}; diff --git a/esphome/core/config.py b/esphome/core/config.py index bf0beea2ef..2057ad356e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -19,6 +19,7 @@ from esphome.const import ( CONF_LIBRARIES, CONF_MIN_VERSION, CONF_NAME, + CONF_FRIENDLY_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, @@ -124,6 +125,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, + cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string, cv.Optional(CONF_COMMENT): cv.string, cv.Required(CONF_BUILD_PATH): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( @@ -192,6 +194,7 @@ def preload_core_config(config, result): conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME]) CORE.name = conf[CONF_NAME] + CORE.friendly_name = conf.get(CONF_FRIENDLY_NAME, CORE.name) CORE.data[KEY_CORE] = {} if CONF_BUILD_PATH not in conf: @@ -346,6 +349,7 @@ async def to_code(config): cg.add( cg.App.pre_setup( config[CONF_NAME], + config[CONF_FRIENDLY_NAME], cg.RawExpression('__DATE__ ", " __TIME__'), config[CONF_NAME_ADD_MAC_SUFFIX], ) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index d995614c2a..0c12640a2e 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -40,7 +40,7 @@ from esphome.storage_json import ( from esphome.util import get_serial_ports, shlex_quote from esphome.zeroconf import DashboardImportDiscovery, DashboardStatus, EsphomeZeroconf -from .util import password_hash +from .util import password_hash, friendly_name_slugify _LOGGER = logging.getLogger(__name__) @@ -390,12 +390,24 @@ class WizardRequestHandler(BaseHandler): for k, v in json.loads(self.request.body.decode()).items() if k in ("name", "platform", "board", "ssid", "psk", "password") } + if not kwargs["name"]: + self.set_status(422) + self.set_header("content-type", "application/json") + self.write(json.dumps({"error": "Name is required"})) + return + + kwargs["friendly_name"] = kwargs["name"] + kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) + kwargs["ota_password"] = secrets.token_hex(16) noise_psk = secrets.token_bytes(32) kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() - destination = settings.rel_path(f"{kwargs['name']}.yaml") + filename = f"{kwargs['name']}.yaml" + destination = settings.rel_path(filename) wizard.wizard_write(path=destination, **kwargs) self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": filename})) self.finish() @@ -407,6 +419,7 @@ class ImportRequestHandler(BaseHandler): args = json.loads(self.request.body.decode()) try: name = args["name"] + friendly_name = args.get("friendly_name") imported_device = next( (res for res in IMPORT_RESULT.values() if res.device_name == name), None @@ -414,12 +427,15 @@ class ImportRequestHandler(BaseHandler): if imported_device is not None: network = imported_device.network + if friendly_name is None: + friendly_name = imported_device.friendly_name else: network = const.CONF_WIFI import_config( settings.rel_path(f"{name}.yaml"), name, + friendly_name, args["project_name"], args["package_import_url"], network, @@ -434,6 +450,8 @@ class ImportRequestHandler(BaseHandler): return self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": f"{name}.yaml"})) self.finish() @@ -581,6 +599,12 @@ class DashboardEntry: return self.filename.replace(".yml", "").replace(".yaml", "") return self.storage.name + @property + def friendly_name(self): + if self.storage is None: + return self.name + return self.storage.friendly_name + @property def comment(self): if self.storage is None: @@ -628,6 +652,7 @@ class ListDevicesHandler(BaseHandler): "configured": [ { "name": entry.name, + "friendly_name": entry.friendly_name, "configuration": entry.filename, "loaded_integrations": entry.loaded_integrations, "deployed_version": entry.update_old, @@ -643,6 +668,7 @@ class ListDevicesHandler(BaseHandler): "importable": [ { "name": res.device_name, + "friendly_name": res.friendly_name, "package_import_url": res.package_import_url, "project_name": res.project_name, "project_version": res.project_version, diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py index 3e3864aa17..a2ad530b74 100644 --- a/esphome/dashboard/util.py +++ b/esphome/dashboard/util.py @@ -1,4 +1,7 @@ import hashlib +import unicodedata + +from esphome.const import ALLOWED_NAME_CHARS def password_hash(password: str) -> bytes: @@ -7,3 +10,23 @@ def password_hash(password: str) -> bytes: Note this is not meant for secure storage, but for securely comparing passwords. """ return hashlib.sha256(password.encode()).digest() + + +def strip_accents(value): + return "".join( + c + for c in unicodedata.normalize("NFD", str(value)) + if unicodedata.category(c) != "Mn" + ) + + +def friendly_name_slugify(value): + value = ( + strip_accents(value) + .lower() + .replace(" ", "-") + .replace("_", "-") + .replace("--", "-") + .strip("-") + ) + return "".join(c for c in value if c in ALLOWED_NAME_CHARS) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index c2c8b91a36..bbdfbbc8a2 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -36,6 +36,7 @@ class StorageJSON: self, storage_version, name, + friendly_name, comment, esphome_version, src_version, @@ -51,6 +52,8 @@ class StorageJSON: self.storage_version: int = storage_version # The name of the node self.name: str = name + # The friendly name of the node + self.friendly_name: str = friendly_name # The comment of the node self.comment: str = comment # The esphome version this was compiled with @@ -77,6 +80,7 @@ class StorageJSON: return { "storage_version": self.storage_version, "name": self.name, + "friendly_name": self.friendly_name, "comment": self.comment, "esphome_version": self.esphome_version, "src_version": self.src_version, @@ -106,6 +110,7 @@ class StorageJSON: return StorageJSON( storage_version=1, name=esph.name, + friendly_name=esph.friendly_name, comment=esph.comment, esphome_version=const.__version__, src_version=1, @@ -118,10 +123,13 @@ class StorageJSON: ) @staticmethod - def from_wizard(name: str, address: str, platform: str) -> "StorageJSON": + def from_wizard( + name: str, friendly_name: str, address: str, platform: str + ) -> "StorageJSON": return StorageJSON( storage_version=1, name=name, + friendly_name=friendly_name, comment=None, esphome_version=None, src_version=1, @@ -139,6 +147,7 @@ class StorageJSON: storage = json.load(f_handle) storage_version = storage["storage_version"] name = storage.get("name") + friendly_name = storage.get("friendly_name") comment = storage.get("comment") esphome_version = storage.get( "esphome_version", storage.get("esphomeyaml_version") @@ -153,6 +162,7 @@ class StorageJSON: return StorageJSON( storage_version, name, + friendly_name, comment, esphome_version, src_version, diff --git a/esphome/wizard.py b/esphome/wizard.py index 0fcccfc3f6..fd661af639 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -46,6 +46,11 @@ BASE_CONFIG = """esphome: name: {name} """ +BASE_CONFIG_FRIENDLY = """esphome: + name: {name} + friendly_name: {friendly_name} +""" + LOGGER_API_CONFIG = """ # Enable logging logger: @@ -110,7 +115,12 @@ def wizard_file(**kwargs): kwargs["fallback_name"] = ap_name kwargs["fallback_psk"] = "".join(random.choice(letters) for _ in range(12)) - config = BASE_CONFIG.format(**kwargs) + if kwargs.get("friendly_name"): + base = BASE_CONFIG_FRIENDLY + else: + base = BASE_CONFIG + + config = base.format(**kwargs) config += HARDWARE_BASE_CONFIGS[kwargs["platform"]].format(**kwargs) @@ -192,7 +202,7 @@ def wizard_write(path, **kwargs): hardware = kwargs["platform"] write_file(path, wizard_file(**kwargs)) - storage = StorageJSON.from_wizard(name, f"{name}.local", hardware) + storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage.save(storage_path) diff --git a/esphome/zeroconf.py b/esphome/zeroconf.py index 3743f650f3..1ba8397e14 100644 --- a/esphome/zeroconf.py +++ b/esphome/zeroconf.py @@ -119,10 +119,12 @@ TXT_RECORD_PACKAGE_IMPORT_URL = b"package_import_url" TXT_RECORD_PROJECT_NAME = b"project_name" TXT_RECORD_PROJECT_VERSION = b"project_version" TXT_RECORD_NETWORK = b"network" +TXT_RECORD_FRIENDLY_NAME = b"friendly_name" @dataclass class DiscoveredImport: + friendly_name: Optional[str] device_name: str package_import_url: str project_name: str @@ -174,8 +176,12 @@ class DashboardImportDiscovery: project_name = info.properties[TXT_RECORD_PROJECT_NAME].decode() project_version = info.properties[TXT_RECORD_PROJECT_VERSION].decode() network = info.properties.get(TXT_RECORD_NETWORK, b"wifi").decode() + friendly_name = info.properties.get(TXT_RECORD_FRIENDLY_NAME) + if friendly_name is not None: + friendly_name = friendly_name.decode() self.import_state[name] = DiscoveredImport( + friendly_name=friendly_name, device_name=node_name, package_import_url=import_url, project_name=project_name, diff --git a/requirements.txt b/requirements.txt index 382d3fe2a0..1c5ae391e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ pyserial==3.5 platformio==6.1.5 # When updating platformio, also update Dockerfile esptool==4.4 click==8.1.3 -esphome-dashboard==20221231.0 +esphome-dashboard==20230117.0 aioesphomeapi==13.0.2 zeroconf==0.47.1 diff --git a/tests/dummy_main.cpp b/tests/dummy_main.cpp index 8cc1838d94..c49a67c9a2 100644 --- a/tests/dummy_main.cpp +++ b/tests/dummy_main.cpp @@ -12,7 +12,7 @@ using namespace esphome; void setup() { - App.pre_setup("livingroom", __DATE__ ", " __TIME__, false); + App.pre_setup("livingroom", "LivingRoom", __DATE__ ", " __TIME__, false); auto *log = new logger::Logger(115200, 512); // NOLINT log->pre_setup(); log->set_uart_selection(logger::UART_SELECTION_UART0);