Add translation keys for well-known mobile_app sensors

Mobile app sensors are dynamically registered by the iOS and Android
Companion apps with whatever name/icon/state values the app supplies.
For a curated set of sensor unique IDs that the apps emit consistently,
let HA own the name (via translation_key + has_entity_name), the icon
(via icons.json) and the state (via state translations).

For known sensors the app-supplied name and icon are ignored. All other
sensors keep the existing behaviour. The initial set covers
battery_state, charger_type, ringer_mode and audio_mode.
This commit is contained in:
Claude
2026-04-29 21:25:58 +00:00
parent fe577eaa1f
commit 32d7fa348e
5 changed files with 116 additions and 7 deletions
+12 -2
View File
@@ -26,11 +26,13 @@ from .const import (
ATTR_SENSOR_STATE,
ATTR_SENSOR_STATE_CLASS,
ATTR_SENSOR_TYPE,
ATTR_SENSOR_UNIQUE_ID,
DATA_PENDING_UPDATES,
DOMAIN,
SIGNAL_SENSOR_UPDATE,
)
from .helpers import device_info
from .known_sensors import get_translation_key
_LOGGER = logging.getLogger(__name__)
@@ -49,7 +51,14 @@ class MobileAppEntity(RestoreEntity):
self._attr_entity_registry_enabled_default = not config.get(
ATTR_SENSOR_DISABLED
)
self._attr_name = config[CONF_NAME]
translation_key = get_translation_key(
config[ATTR_SENSOR_TYPE], config[ATTR_SENSOR_UNIQUE_ID]
)
if translation_key is not None:
self._attr_has_entity_name = True
self._attr_translation_key = translation_key
else:
self._attr_name = config[CONF_NAME]
self._async_update_attr_from_config()
@callback
@@ -59,7 +68,8 @@ class MobileAppEntity(RestoreEntity):
self._attr_device_class = config.get(ATTR_SENSOR_DEVICE_CLASS)
self._attr_state_class = config.get(ATTR_SENSOR_STATE_CLASS)
self._attr_extra_state_attributes = config[ATTR_SENSOR_ATTRIBUTES]
self._attr_icon = config[ATTR_SENSOR_ICON]
if self._attr_translation_key is None:
self._attr_icon = config[ATTR_SENSOR_ICON]
self._attr_entity_category = config.get(ATTR_SENSOR_ENTITY_CATEGORY)
self._attr_available = config.get(ATTR_SENSOR_STATE) != STATE_UNAVAILABLE
@@ -4,6 +4,20 @@
"notify": {
"default": "mdi:cellphone-message"
}
},
"sensor": {
"audio_mode": {
"default": "mdi:volume-high"
},
"battery_state": {
"default": "mdi:battery"
},
"charger_type": {
"default": "mdi:power-plug"
},
"ringer_mode": {
"default": "mdi:bell-ring"
}
}
}
}
@@ -0,0 +1,27 @@
"""Known sensor unique IDs that have translation support.
Mobile app sensors are dynamically registered by the companion apps. For a
curated set of sensor IDs that the iOS and Android Companion apps use
consistently we provide HA-side translations for the entity name, icon and
(when applicable) state. For all other sensor IDs the integration keeps using
the name/icon supplied by the app.
"""
from __future__ import annotations
from .const import ATTR_SENSOR_TYPE_SENSOR
# Mapping of (entity_type, sensor_unique_id) to translation_key.
# The translation_key must have a matching entry in strings.json under
# entity.<entity_type>.<translation_key>.
KNOWN_SENSORS: dict[tuple[str, str], str] = {
(ATTR_SENSOR_TYPE_SENSOR, "battery_state"): "battery_state",
(ATTR_SENSOR_TYPE_SENSOR, "charger_type"): "charger_type",
(ATTR_SENSOR_TYPE_SENSOR, "ringer_mode"): "ringer_mode",
(ATTR_SENSOR_TYPE_SENSOR, "audio_mode"): "audio_mode",
}
def get_translation_key(entity_type: str, sensor_unique_id: str) -> str | None:
"""Return the translation key for a known sensor, or None."""
return KNOWN_SENSORS.get((entity_type, sensor_unique_id))
@@ -18,6 +18,47 @@
"title": "Title"
}
},
"entity": {
"sensor": {
"audio_mode": {
"name": "Audio mode",
"state": {
"normal": "Normal",
"ringing": "Ringing",
"in_call": "In call",
"in_communication": "In communication",
"call_screening": "Call screening"
}
},
"battery_state": {
"name": "Battery state",
"state": {
"charging": "Charging",
"discharging": "Discharging",
"full": "Full",
"not_charging": "Not charging",
"unknown": "Unknown"
}
},
"charger_type": {
"name": "Charger type",
"state": {
"ac": "AC",
"usb": "USB",
"wireless": "Wireless",
"none": "None"
}
},
"ringer_mode": {
"name": "Ringer mode",
"state": {
"normal": "Normal",
"silent": "Silent",
"vibrate": "Vibrate"
}
}
}
},
"exceptions": {
"device_not_connected_for_local_push_notifications": {
"message": "Device {device_name} is not connected for local push notifications"
+22 -5
View File
@@ -120,6 +120,7 @@ from .helpers import (
safe_registration,
webhook_response,
)
from .known_sensors import get_translation_key
_LOGGER = logging.getLogger(__name__)
@@ -567,6 +568,8 @@ async def webhook_register_sensor(
data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID]
translation_key = get_translation_key(entity_type, unique_id)
# If sensor already is registered, update current state instead
if existing_sensor:
_LOGGER.debug(
@@ -577,10 +580,25 @@ async def webhook_register_sensor(
assert entry is not None
changes: dict[str, Any] = {}
if (
new_name := f"{device_name} {data[ATTR_SENSOR_NAME]}"
) != entry.original_name:
changes["original_name"] = new_name
if translation_key is not None:
# For known sensors HA owns the name and icon via translations,
# so we ignore the values supplied by the mobile app.
if entry.translation_key != translation_key:
changes["translation_key"] = translation_key
if entry.original_name is not None:
changes["original_name"] = None
if entry.original_icon is not None:
changes["original_icon"] = None
else:
if (
new_name := f"{device_name} {data[ATTR_SENSOR_NAME]}"
) != entry.original_name:
changes["original_name"] = new_name
if (
ATTR_SENSOR_ICON in data
and entry.original_icon != data[ATTR_SENSOR_ICON]
):
changes["original_icon"] = data[ATTR_SENSOR_ICON]
if (
should_be_disabled := data.get(ATTR_SENSOR_DISABLED)
@@ -595,7 +613,6 @@ async def webhook_register_sensor(
("device_class", ATTR_SENSOR_DEVICE_CLASS),
("unit_of_measurement", ATTR_SENSOR_UOM),
("entity_category", ATTR_SENSOR_ENTITY_CATEGORY),
("original_icon", ATTR_SENSOR_ICON),
):
if data_key in data and getattr(entry, ent_reg_key) != data[data_key]:
changes[ent_reg_key] = data[data_key]