Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions examples/multi-device/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,47 @@ transport profile. The API profile clears a Storage latch left by the MQTT
profile and keeps the common low-battery latch. Keep credentials in the local `secrets.yaml`; remote package
files contain no secrets.

The MQTT example preserves an explicit topic prefix. Treat that prefix as a
stable identity after Home Assistant discovery, because changing it creates new
entities.
## Identity and migration

New installations use function-only entity names and MAC-based MQTT discovery
identifiers. `display_name` sets the human device label. It defaults to the
existing `friendly_name` substitution for compatibility. Changing that label
does not change the configured name or the entity-name hash. The configured
name defaults to `device_name`, with ESPHome's MAC suffix enabled. The default
MQTT topic prefix follows this effective node name.

For duplicate plant species, set a unique `configured_name` before building.
To preserve an existing MAC-bearing name exactly, set that complete name and
`name_add_mac_suffix: "false"`. Never change a deployed technical identity as
part of a display-only rename.

**Installed devices require an explicit migration.** These new defaults change
legacy entity names and MQTT discovery unique IDs. HA can create new entities;
this PR does not migrate its registry, automations, dashboards or history.
Keep the previous package revision until you have planned that migration.
Preserve your existing command topics, and remove old retained discovery only
as part of your own reviewed migration. Test one device first.

To keep the previous package behavior while upgrading, override the named
entities in your device YAML (ESPHome merges them by `id`) and preserve the MQTT
settings explicitly. For example, for the battery entity:

```yaml
sensor:
- id: batpercent
name: "${friendly_name} Battery"
mqtt:
topic_prefix: "your-existing-prefix"
discovery_unique_id_generator: legacy
discovery_object_id_generator: none
```

Repeat the entity override for every existing named sensor, text sensor and
switch. The old `mqtt_topic_prefix` substitution alone no longer changes
`mqtt.topic_prefix`; use the explicit block above. This preserves the original
strings; it does not rename or repair already migrated HA entities. With MAC
identifiers, changing an entity's own name still changes its name hash. Keep
those function names stable too.

Both profiles use the released `waveshare_epaper` driver and wait for the
panel's physical BUSY cycle before deep sleep. They deliberately keep the
Expand Down Expand Up @@ -80,7 +118,7 @@ Publish `ON` or `OFF` with retain enabled and QoS 1:
| Maintenance | `<prefix>/cmd/maintenance` | `<prefix>/status/maintenance` |
| Storage | `<prefix>/cmd/storage_mode` | `<prefix>/status/storage_mode` |

`<prefix>` is `mqtt_topic_prefix`. Command state expresses the requested mode.
`<prefix>` is the effective MQTT topic prefix (the MAC-bearing node name by default). Command state expresses the requested mode.
Result state expresses the accepted mode. Home Assistant discovers both
controls and their diagnostic result entities.

Expand Down
5 changes: 2 additions & 3 deletions examples/multi-device/my-lemon-tree.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@ substitutions:
mqtt_password: !secret mqtt_password
api_encryption_key: !secret lemon_tree_api_encryption_key

# MQTT topic prefix — set once and never change (renaming orphans HA entities).
# Format: <device_name>-<last 3 bytes of MAC>, visible in ESPHome logs on first boot.
mqtt_topic_prefix: "lemon-tree-abcdef"
# The default MQTT prefix is the effective node name, including its MAC suffix.
# Keep the node identity stable after discovery; see README migration guidance.

# OTA password for this device
ota_password: "replace-with-your-ota-password"
Expand Down
34 changes: 20 additions & 14 deletions examples/multi-device/packages/smart_plant_core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
# Default values — override any of these in your device file.
substitutions:
device_name: "smart-plant"
# Preserve the technical identity when changing display text.
configured_name: "${device_name}"
name_add_mac_suffix: "true"
# Compatibility input: existing friendly_name overrides still supply the label.
friendly_name: "Smart Plant"
display_name: "${friendly_name}"
device_comment: "${friendly_name}"
project_name: "smart.plant"
project_version: "2.2"
Expand Down Expand Up @@ -110,8 +115,9 @@ globals:
initial_value: 'true'

esphome:
name: "${device_name}"
name_add_mac_suffix: true
name: "${configured_name}"
name_add_mac_suffix: "${name_add_mac_suffix}"
friendly_name: "${display_name}"
comment: "${device_comment}"
project:
name: "${project_name}"
Expand Down Expand Up @@ -205,27 +211,27 @@ sensor:
update_interval: never
battery_voltage:
id: batvolt
name: "${friendly_name} Battery voltage"
name: "Battery voltage"
internal: true
battery_level:
id: batpercent
device_class: "battery"
unit_of_measurement: "%"
name: "${friendly_name} Battery"
name: "Battery"
force_update: true

- platform: aht10
id: aht20_sensor
variant: AHT20
i2c_id: bus_a
temperature:
name: "${friendly_name} Temperature"
name: "Temperature"
id: temp
icon: "mdi:thermometer"
device_class: temperature
force_update: true
humidity:
name: "${friendly_name} Air Humidity"
name: "Air Humidity"
id: hum
icon: "mdi:water-percent"
device_class: humidity
Expand All @@ -237,18 +243,18 @@ sensor:
address: 0x10
update_interval: never
ambient_light:
name: "${friendly_name} Ambient light"
name: "Ambient light"
id: light
icon: "mdi:white-balance-sunny"
device_class: illuminance
force_update: true
actual_gain:
name: "${friendly_name} Actual gain"
name: "Actual gain"
internal: true

- platform: adc
pin: GPIO1
name: "${friendly_name} Soil Voltage"
name: "Soil Voltage"
id: soil_voltage
device_class: voltage
state_class: measurement
Expand All @@ -275,7 +281,7 @@ sensor:
# sampling loop or an intermediate unfiltered percentage.
- platform: copy
source_id: soil_voltage
name: "${friendly_name} Soil Moisture"
name: "Soil Moisture"
id: soil
icon: "mdi:cup-water"
device_class: moisture
Expand All @@ -291,7 +297,7 @@ sensor:
# Publish the running firmware version and native effective-configuration hash.
text_sensor:
- platform: version
name: "${friendly_name} ESPHome Version"
name: "ESPHome Version"
hide_timestamp: true
hide_hash: false
entity_category: diagnostic
Expand Down Expand Up @@ -374,7 +380,7 @@ display:

- id: page_maintenance
lambda: |-
it.printf(148, 8, id(font_subtitle), TextAlign::TOP_CENTER, "${friendly_name}");
it.printf(148, 8, id(font_subtitle), TextAlign::TOP_CENTER, "${display_name}");
it.printf(148, 38, id(font_title), TextAlign::TOP_CENTER, "MAINTENANCE");
if (id(maintenance_end_hour) >= 0) {
it.printf(148, 82, id(font_title), TextAlign::TOP_CENTER, "ENDS AT %02d:%02d",
Expand All @@ -386,7 +392,7 @@ display:

- id: page_storage
lambda: |-
it.printf(148, 4, id(font_subtitle), TextAlign::TOP_CENTER, "${friendly_name}");
it.printf(148, 4, id(font_subtitle), TextAlign::TOP_CENTER, "${display_name}");
it.printf(148, 31, id(font_title), TextAlign::TOP_CENTER, "STORAGE MODE");
it.printf(148, 68, id(font_subtitle), TextAlign::TOP_CENTER, "DAILY CHECK");
if (id(batpercent).has_state() && std::isfinite(id(batpercent).state)) {
Expand All @@ -401,7 +407,7 @@ display:
- id: page_low_batt
lambda: |-
it.fill(COLOR_ON);
it.printf(148, 4, id(font_subtitle), COLOR_OFF, TextAlign::TOP_CENTER, "${friendly_name}");
it.printf(148, 4, id(font_subtitle), COLOR_OFF, TextAlign::TOP_CENTER, "${display_name}");
it.printf(148, 26, id(font_title), COLOR_OFF, TextAlign::TOP_CENTER, "LOW BATT !");
if (id(storage_active)) {
it.printf(148, 60, id(font_subtitle), COLOR_OFF, TextAlign::TOP_CENTER, "STORAGE MODE");
Expand Down
5 changes: 3 additions & 2 deletions examples/multi-device/packages/smart_plant_profile_mqtt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# =============================================================================

substitutions:
mqtt_topic_prefix: "${device_name}"
mqtt_broker: ""
mqtt_username: ""
mqtt_password: ""
Expand Down Expand Up @@ -79,12 +78,14 @@ esphome:
# or you get duplicate entities that go unavailable during sleep. Keep them on MQTT.
mqtt:
id: mqtt_client
topic_prefix: "${mqtt_topic_prefix}"
broker: "${mqtt_broker}"
username: "${mqtt_username}"
password: "${mqtt_password}"
discovery: true
discovery_retain: true
# Per-device identity. Keep function-only entity names stable across renames.
discovery_unique_id_generator: mac
discovery_object_id_generator: device_name
birth_message: # disabled — prevents HA marking device offline during sleep
will_message:
log_topic: # disabled — saves bandwidth on wake
Expand Down
41 changes: 41 additions & 0 deletions tests/test_identity_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Validate rename behavior through ESPHome's real package/substitution loader."""
import importlib.util
from pathlib import Path
import tempfile
import unittest
import yaml
ROOT = Path(__file__).resolve().parents[1]
@unittest.skipUnless(importlib.util.find_spec("esphome"), "ESPHome is required")
class EntityIdentityTests(unittest.TestCase):
def test_display_rename_preserves_function_names_and_technical_identity(self):
from esphome.core import CORE
from esphome.config import read_config
results=[]
try:
with tempfile.TemporaryDirectory() as directory:
for label in ("First label", "Changed label"):
values={"device_name":"schema-plant", "configured_name":"schema-plant-abcdef",
"name_add_mac_suffix":"false", "friendly_name":label,
"wifi_ssid":"synthetic-wifi", "wifi_password":"synthetic-password",
"mqtt_broker":"192.0.2.1", "ota_password":"synthetic-ota",
"api_encryption_key":"AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
"label_image":str(ROOT/"docs/source/images/programming/Lemon_tree_label_page_1.png")}
source=yaml.safe_dump({"substitutions":values},sort_keys=False)
source+="packages:\n core: !include "+str(ROOT/"examples/multi-device/packages/smart_plant_core.yaml")+"\n transport: !include "+str(ROOT/"examples/multi-device/packages/smart_plant_profile_mqtt.yaml")+"\n"
path=Path(directory)/"schema.yaml";path.write_text(source)
CORE.reset();CORE.config_path=path;config=read_config({})
self.assertIsNotNone(config)
results.append({"name":config["esphome"]["name"],
"friendly":config["esphome"]["friendly_name"],
"sensor_names":{str(s["id"]):s.get("name") for s in config["sensor"] if "name" in s},
"switch_names":{str(s["id"]):s.get("name") for s in config["switch"] if "name" in s},
"prefix":config["mqtt"]["topic_prefix"]})
self.assertEqual(str(config["mqtt"]["discovery_unique_id_generator"]),"mac")
self.assertEqual(str(config["mqtt"]["discovery_object_id_generator"]),"device_name")
self.assertNotEqual(results[0].pop("friendly"),results[1].pop("friendly"))
self.assertEqual(results[0],results[1])
self.assertEqual(results[0]["name"],"schema-plant-abcdef")
self.assertEqual(results[0]["prefix"],"schema-plant-abcdef")
self.assertEqual(results[0]["sensor_names"], results[1]["sensor_names"])
finally:
CORE.reset()
Loading