Add friendly_name to device (#4296)

This commit is contained in:
Jesse Hills 2023-01-17 10:28:09 +13:00 committed by GitHub
parent 3d2d681a7b
commit c301ae3645
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 137 additions and 18 deletions

View file

@ -206,6 +206,8 @@ message DeviceInfoResponse {
uint32 bluetooth_proxy_version = 11;
string manufacturer = 12;
string friendly_name = 13;
}
message ListEntitiesRequest {

View file

@ -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();

View file

@ -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

View file

@ -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;

View file

@ -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)

View file

@ -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;

View file

@ -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(),

View file

@ -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"

View file

@ -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

View file

@ -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};

View file

@ -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],
)

View file

@ -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,

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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,

View file

@ -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

View file

@ -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);