Date: 2026-04-02
Scope: Runtime behavior when companion or remote node configuration is changed via execute_command service
Integration version: Current dev/combined branch
Firmware reference: MeshCore v1.14.1 companion protocol source (MyMesh.cpp, CommonCLI.cpp)
When a user changes device configuration at runtime — for example, adjusting radio frequency, renaming a node, or setting coordinates — the MeshCore firmware applies the change immediately. However, the HA integration does not reflect these changes in entity states, the device registry, or entity identifiers. This creates a confusing user experience where the device has changed but Home Assistant still shows the old values, and in some cases creates a structural risk where future name changes could produce duplicate entities.
This report documents four related issues, traces each through the firmware and integration code, and proposes a phased fix.
Scenario 1 — Radio settings change:
A user calls meshcore.execute_command with set_radio(906.875, 250.0, 11, 5) to change their companion's radio parameters. The firmware applies the change to the LoRa chip immediately (confirmed in firmware source — CMD_SET_RADIO_PARAMS calls both savePrefs() and radio_set_params()). But the HA sensors for frequency, bandwidth, and spreading factor continue showing the old values. Dashboard cards, automations triggered by radio config, and template sensors all see stale data. The values don't update until the integration is reloaded or HA is restarted.
Scenario 2 — Device rename:
A user renames their companion from "Matt's Gateway" to "Otay Gateway" using set_name. The firmware updates the name in flash. But the HA device registry still shows "Matt's Gateway", entity IDs still contain matt_s_gateway, and the config entry still stores the old name. A subsequent integration reload won't fix it either — the name in config_entry.data is only written during the config flow.
Scenario 3 — Repeater rename:
A repeater owner renames their device. The integration detects the new name via mesh advertisements (NEW_CONTACT events) and contact syncs. But the repeater's tracked subscription config, entity IDs, unique_ids, and device registry name all retain the original name captured during config flow setup.
Scenario 4 — Name change causes entity duplication (structural risk):
If any future code change causes entity reconstruction with an updated name, HA would create new entities (with the new name in unique_id) while orphaning the old ones — because six entity classes currently include the device name in their unique_id. HA uses unique_id as the entity's permanent identity. Changing it means HA sees a different entity, not an update to an existing one.
When the integration calls a configuration command via execute_command (e.g., set_radio, set_tx_power, set_name, set_coords), the command executes on the device but the integration never requests the device's updated configuration afterward.
The companion protocol provides a lightweight mechanism for this: send_appstart() sends CMD_APP_START and receives a SELF_INFO response containing the device's full current configuration. This is the same call used during connection setup. It does not disconnect, restart, or reset any state — it simply reads back the current _prefs values from the firmware.
The integration's sensor entities already subscribe to SELF_INFO events and are wired to update from them. The missing piece is that execute_command in services.py never triggers a send_appstart() after config-changing commands, so the event never fires.
Existing precedent: The execute_command service already has post-command hooks for set_channel (refreshes channel info via get_channel), add_contact, and remove_contact (marks contacts dirty and triggers coordinator update). The pattern exists — it just wasn't applied to config commands.
Commands that modify SELF_INFO fields (all need the refresh):
| Command | Fields Changed |
|---|---|
set_radio |
radio_freq, radio_bw, radio_sf, radio_cr, client_repeat |
set_tx_power |
tx_power |
set_name |
name |
set_coords |
adv_lat, adv_lon |
set_multi_acks |
multi_acks |
set_advert_loc_policy |
adv_loc_policy |
set_path_hash_mode |
path_hash_mode |
set_telemetry_mode_base |
telemetry_mode (bits 0-1) |
set_telemetry_mode_loc |
telemetry_mode (bits 2-3) |
set_telemetry_mode_env |
telemetry_mode (bits 4-7) |
set_manual_add_contacts |
manual_add_contacts |
import_private_key |
public_key (after reboot) |
All downstream consumers that would benefit from the refresh:
| Consumer | File | Reads | Current Status |
|---|---|---|---|
| Frequency sensor | sensor.py | radio_freq |
Subscribed to SELF_INFO, but event never fires after changes |
| Bandwidth sensor | sensor.py | radio_bw |
Same |
| Spreading Factor sensor | sensor.py | radio_sf |
Same |
| TX Power sensor | sensor.py | tx_power |
Same |
| Latitude sensor | sensor.py | adv_lat |
Same |
| Longitude sensor | sensor.py | adv_lon |
Same |
| CompanionPrefixSensor | sensor.py | public_key, path_hash_mode |
Same (but this one correctly calls async_write_ha_state()) |
| Map uploader | map_uploader.py | radio_freq/bw/sf/cr |
Subscribed via __init__.py, caches radio params |
| MQTT uploader | mqtt_uploader.py | radio_freq/bw/sf/cr, name |
Updates status metadata and node name |
| API cache | meshcore_api.py | Entire payload | Caches in _last_self_info |
Even if Issue 1 were fixed and SELF_INFO events fired after config changes, six sensor entities would still not propagate the new values to HA immediately. Their event handlers update self._native_value but do not call self.async_write_ha_state() to notify HA that the state changed.
Affected sensors:
| Sensor Key | Handler Location |
|---|---|
tx_power |
sensor.py:888-894 |
latitude |
sensor.py:896-902 |
longitude |
sensor.py:904-910 |
frequency |
sensor.py:912-918 |
bandwidth |
sensor.py:921-927 |
spreading_factor |
sensor.py:929-935 |
Correct implementations in the same codebase (for comparison):
node_counthandler (sensor.py:878) — correctly callsself.async_write_ha_state()CompanionPrefixSensor.update_from_self_info()(sensor.py:1003) — correctly callsself.async_write_ha_state()
Without this call, HA doesn't know the entity value changed. The value sits in _native_value until the next coordinator poll cycle happens to trigger a state write. Dashboard cards, automations, and template sensors see stale values for an unpredictable duration.
This is the deepest issue and affects both the companion device and remote nodes (repeaters/clients).
The device name enters the integration during config flow and is stored in config_entry.data["name"]. At integration startup:
coordinator.__init__reads it intoself.name(coordinator.py:88)coordinator.__init__buildsself.device_infowith"name": f"MeshCore {self.name} ({pubkey[:6]})"(coordinator.py:100)- Entity classes read
coordinator.nameduring their__init__and bake it into_attr_unique_idandentity_id
None of these values are ever updated after construction. There is no listener for name changes, no config_entry update mechanism, and no entity migration path.
For remote nodes, the pattern is identical: the name captured during config flow is stored in config_entry.data["repeater_subscriptions"][i]["name"] and baked into entity identity at construction.
The integration maintains two separate stores of remote node names that never synchronize:
| Store | Updated at Runtime? | Used For |
|---|---|---|
config_entry.data["repeater_subscriptions"][i]["name"] |
No — set once during config flow | Entity IDs, unique_ids, device_info |
coordinator._discovered_contacts[pubkey]["adv_name"] |
Yes — updated on every advertisement | get_all_contacts() display data |
coordinator._contacts[prefix]["adv_name"] |
Yes — updated on every ensure_contacts() |
get_all_contacts() display data |
When a repeater changes its advertised name, the contact objects reflect the new name, but everything built from the config flow snapshot (entity IDs, device registry, subscription config) remains stale.
Six entity classes include the device name in their unique_id. This violates HA's entity identity model — unique_id should use only stable identifiers (entry_id, pubkey, description key) so that entities survive configuration changes without duplication.
If a name change were to cause entity reconstruction with the new name, HA would create new entities alongside the old ones rather than updating them, because the unique_id would be different.
Entity classes with name in unique_id (needs migration):
| Entity Class | Current unique_id Pattern |
Stable Alternative |
|---|---|---|
MeshCoreSensor |
{entry_id}_{key}_{pubkey[:6]}_{name} |
{entry_id}_{key}_{pubkey[:6]} |
RateLimiterSensor |
{entry_id}_rate_limiter_tokens_{pubkey[:6]}_{name} |
{entry_id}_rate_limiter_tokens_{pubkey[:6]} |
LastMessageDeliverySensor |
{entry_id}_last_message_delivery_{pubkey[:6]}_{name} |
{entry_id}_last_message_delivery_{pubkey[:6]} |
MeshCoreReliabilitySensor |
{entry_id}_{type}_{pubkey}_{key}_{pubkey[:6]}_{name} |
{entry_id}_{type}_{pubkey}_{key}_{pubkey[:6]} |
MeshCorePathSensor |
{entry_id}_{type}_{pubkey}_{key}_{pubkey[:6]}_{name} |
{entry_id}_{type}_{pubkey}_{key}_{pubkey[:6]} |
MeshCoreRepeaterSensor |
{entry_id}_repeater_{pubkey}_{key}_{pubkey[:6]}_{name} |
{entry_id}_repeater_{pubkey}_{key}_{pubkey[:6]} |
Entity classes already using stable unique_id (no changes needed):
| Entity Class | unique_id Pattern |
|---|---|
MeshCoreCompanionPrefixSensor |
{entry_id}_companion_prefix_{pubkey[:6]} |
MeshCoreMessageEntity |
{entry_id}_{pubkey[:6]}_{entity_key[:6]}_messages |
MeshCoreMqttBrokerConnectionBinarySensor |
{entry_id}_mqtt_broker_{num}_connection |
MeshCoreContactDiagnosticBinarySensor |
{pubkey[:12]} |
MeshCoreGPSTracker |
{entry_id}_{pubkey}_gps_tracker |
MeshCoreTelemetrySensor |
{entry_id}_{pubkey}_{channel}_{lpp_type}_telemetry |
| All Select entities | {entry_id}_{type}_select |
| All Text entities | {entry_id}_{type}_input |
Existing precedent: The integration already has a _migrate_entity_ids function in __init__.py that migrates entity IDs and unique_ids when a public key change is detected. The same pattern applies here.
map_uploader.update_self_info() only caches radio parameters (radio_freq, radio_bw, radio_sf, radio_cr). It does not cache adv_lat/adv_lon. If coordinates change, the map uploader continues using whatever values it had at connection time.
The MeshCoreGPSTracker entity is unaffected — it correctly reads from GPS telemetry events (TELEMETRY_RESPONSE), not from SELF_INFO. The advertised coordinates (adv_lat/adv_lon) and GPS telemetry coordinates serve different purposes: advertised coordinates are what the device tells the mesh network, while GPS telemetry is the device's actual position.
This section documents the firmware's actual behavior for companion protocol commands, confirmed by reading the source. This is important because some documentation (including the CLI docs and the MeshCore-Commands-Reference.md) states that set radio "requires reboot to apply." That is true for the CLI text command path (CommonCLI.cpp — only calls savePrefs(), replies "OK - reboot to apply") but not for the companion binary protocol path (MyMesh.cpp — calls both savePrefs() and radio_set_params()).
The Python library uses the companion binary protocol. Changes take effect immediately.
| Companion Protocol Command | Firmware Behavior | Connection Impact |
|---|---|---|
CMD_SET_RADIO_PARAMS (0x0B) |
savePrefs() + radio_set_params() — applies immediately to LoRa chip |
None — BLE/serial is separate from LoRa |
CMD_SET_RADIO_TX_POWER (0x0C) |
savePrefs() + radio_set_tx_power() — applies immediately |
None |
CMD_SET_NAME |
savePrefs() — name stored in flash |
None |
CMD_SET_COORDS |
Updates sensors.node_lat/lon — applies immediately |
None |
CMD_APP_START (0x01) |
Returns SELF_INFO frame with current config from _prefs |
None — lightweight read-only query |
send_appstart() / CMD_APP_START is safe to call at any time. It sends one frame, receives one frame, and has no side effects beyond clearing any in-progress contacts iterator.
| # | Issue | User Impact | Severity | Fix Complexity |
|---|---|---|---|---|
| 1 | No send_appstart() after config commands in execute_command |
Sensors show stale values after any config change; requires integration reload to see updated values | High | Low |
| 2 | Six sensor event handlers missing async_write_ha_state() |
Even with Issue 1 fixed, HA state updates are delayed until next coordinator poll | Medium | Low |
| 3a | Companion name change not propagated to config_entry, coordinator, device registry | Device name in HA doesn't update when companion is renamed | Medium | Medium |
| 3b | Remote node name change not propagated from contact data to subscription config, entity identity | Repeater/client device names in HA don't update when nodes are renamed | Medium | Medium |
| 3c | Device name included in unique_id for 6 entity classes |
Risk of duplicate/orphaned entities if name changes are ever applied; violates HA entity identity best practice | High | Medium |
| 4 | Map uploader ignores coordinate changes in SELF_INFO | Map uploads may use stale coordinates | Low | Low |
Phase 1 — Config change propagation (Issues 1 + 2):
Add a send_appstart() post-command hook in execute_command for all commands that modify SELF_INFO fields. Add async_write_ha_state() to the six sensor event handlers that are missing it. Low risk, high impact — this makes all config changes immediately visible in HA.
Phase 2 — Name identity stabilization (Issues 3a + 3b + 3c):
Remove device name from unique_id in all six affected entity classes, with a one-time migration to rewrite existing unique_ids in the entity registry. Add runtime name change detection for both companion (via SELF_INFO) and remote nodes (via contact data), propagating changes to config_entry.data, coordinator.device_info, and entity IDs. Follow the existing pubkey migration pattern in __init__.py.
Phase 3 — Map uploader coordinates (Issue 4):
Add adv_lat/adv_lon to map_uploader.update_self_info() key list. Optional, low priority.