Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mwolter805/060566596c3951d45f5acdcf90293dc6 to your computer and use it in GitHub Desktop.

Select an option

Save mwolter805/060566596c3951d45f5acdcf90293dc6 to your computer and use it in GitHub Desktop.
Forensics Report - Companion Config Change Propagation

Forensics — Companion Config Change Propagation

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)


Executive Summary

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.


What the User Sees

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.


Root Cause Analysis

Issue 1: No SELF_INFO Refresh After Config Commands

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

Issue 2: Missing async_write_ha_state() in Sensor Event Handlers

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_count handler (sensor.py:878) — correctly calls self.async_write_ha_state()
  • CompanionPrefixSensor.update_from_self_info() (sensor.py:1003) — correctly calls self.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.

Issue 3: Name Changes Not Propagated to Entity Identity

This is the deepest issue and affects both the companion device and remote nodes (repeaters/clients).

How names are stored and used

The device name enters the integration during config flow and is stored in config_entry.data["name"]. At integration startup:

  1. coordinator.__init__ reads it into self.name (coordinator.py:88)
  2. coordinator.__init__ builds self.device_info with "name": f"MeshCore {self.name} ({pubkey[:6]})" (coordinator.py:100)
  3. Entity classes read coordinator.name during their __init__ and bake it into _attr_unique_id and entity_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 two-store disconnect (remote nodes)

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.

unique_id structural problem

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.

Issue 4: Map Uploader Ignores Coordinate Changes

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.


Firmware Behavior Reference

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.


Summary

# 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

Recommended Fix Priority

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment