In looking through the documentation for home assistant, I discovered that there is no simple and clean way to fully delete devices and/or their associated entities from home assistant. Although it is possible to hide them from the UI, the data and metadata still remain in the homeassistant_v2.db
database and the various registry and restore files in the .storage
directory.
Unfortunately, I found that my homeassistant instance was cluttered with a bunch of “foreign” (i.e. neighbors) MQTT devices & entities as well as legacy devices and entities that are created whenever I change batteries in certain MQTT sensors as well as sensors that I have tested and discarded.
I wanted a clean, safe and easy script that allows one to fully delete all data for individual devices and entities.
So, I created the following bash script that you can run from a ‘ssh’ shell that will do exactly that.
Specifically it cleans up the following:
- Delete devices ‘.storage’ info from:
- core.device_registry
- Delete entity ‘.storage’ info from:
- core.entity_registry
- core.restore_state
- homeassistant.exposed_entities
- Delete corresponding entity data from the following home-assistant_v2.db tables:
- states_meta
- states
- statistics_meta
- statistics
- statistics_short_term
- state_attributes (delete orphan attributes)
The only thing it doesn’t touch are the event data tables although I may add that later…
I tried to have as much error checking as possible as wells as auto-creation of time-stamped backups before any files are modified.
Enjoy! (feedback is welcome!)
#!/bin/bash
#===============================================================================
# DESCRIPTION:
# Delete devices and/or entities *completely from homeassistant registries & databases
# - Delete devices '.storage' info from:
# core.device_registry
# - Delete entity '.storage' info from:
# core.entity_registry
# core.restore_state
# homeassistant.exposed_entities
# - Delete corresponding entity data from the following home-assistant_v2.db tables:
# states_meta
# states
# statistics_meta
# statistics
# statistics_short_term
# state_attributes
# - NOTE: Timestamped backups are generated for every modified database or registry
# - NOTE: Deleted devices can only be accessed by id (-D), not by name
# - NOTE: MQTT devices may consistently be republished if topics are retained.
# So, you need to delete the corresponding topics from the 'homeassistant' tree
# e.g., use MQTT Explorer
# Until then, you may want to stop MQTT from re-adding any existing published
# entities *and* corresponding devices, by adding the following to 'config.yaml'
# mqtt:n
# discovery: false
#
#===============================================================================
# USAGE:
# Usage: delete_device_entity [-d DEV_NAME | -D DEV_ID | -e ENT_NAME | -E ENT_ID ] [-A]
# where you need to specify exactly one of: -d, -D, -e, -E
# If -d or -D specified OR -e or -E plus -A specified, then
# the corresponding device and ALL its entities are deleted
# If -e or -E specified with no '-A', then ONLY the specified entity is deleted
#
#===============================================================================
# VERSION: 0.5
#
#===============================================================================
#
# CHANGELOG
# 0.5 (June 2024)
# - First official release
#===============================================================================
#
# AUTHOR:
# Jeff Kosowsky
# Copyright June 2024
#
#===============================================================================
#### VARIABLES
BASE=/homeassistant
DATABASE="home-assistant_v2.db"
DATABASE="$BASE/$DATABASE"
STORAGE="$BASE/.storage"
DEVREGISTRY=core.device_registry
ENTREGISTRY=core.entity_registry
RESTOREREGISTRY=core.restore_state
EXPOSEDENTITIES=homeassistant.exposed_entities
#===============================================================================
#PARSE OPTIONS
PROG=${0##*/}
usage() {
cat <<EOF >&2
Usage: $PROG [-d DEV_NAME | -D DEV_ID | -e ENT_NAME | -E ENT_ID ] [-A]
where you need to specify exactly one of: -d, -D, -e, -E
If -d or -D specified OR -e or -E plus -A specified, then
the corresponding device and all its entities are deleted
If -e or -E specified with no '-A', then the specified entity alone is deleted
EOF
exit 1
}
NUMDEV=0
while getopts hd:D:e:E:A options; do
case "${options}" in
d) DEVNAME=${OPTARG}
((NUMDEV++))
;;
D) DEVID=${OPTARG}
if ! [[ "$DEVID" =~ ^[[:xdigit:]]{32}$ ]]; then
echo "Error: DEV_ID must be 32 char hex" >&2
usage
fi
((NUMDEV++))
;;
e) ENTNAME=${OPTARG}
((NUMDEV++))
;;
E) ENTID=${OPTARG}
((NUMDEV++))
if ! [[ "$ENTID" =~ ^[[:xdigit:]]{32}$ ]]; then
echo "Error: ENT_ID must be 32 char hex" >&2
usage
fi
;;
A) ALL=1
;;
:) echo "Error: -${OPTARG} requires an argument." >&2
usage
;;
*) usage
;;
esac
done
if [[ $NUMDEV -ne 1 || -n "$ALL" && !( -n "$ENTID" || -n "$ENTNAME" ) ]]; then
echo "Error: Need to enter exactly one type option (-d, -D, -e, -E) plus optional '-A' if entity specified" >&2
usage
fi
shift $((OPTIND-1))
[ -n "$1" ] && usage #No other arguments
#===============================================================================
#### FUNCTIONS
myjoin() {
local separator="$1"
shift
local first="$1"
shift
printf "%s" "$first" "${@/#/$separator}"
}
print_exit() {
echo "*** Don't forget to restart home assistant (ha core start)"
}
#===============================================================================
#### FILE CHECKS
if ! [ -e "$DATABASE" ]; then
echo "Error: Can't find database '$DATABASE'..." >&2
exit 2
fi
if ! [ -d "$STORAGE" ]; then
echo "Error: Can't find storage directory '$STORAGE'..." >&2
exit 3
fi
#===============================================================================
#### Get device and entity data
ha core status >&/dev/null
if [ $? -ne 1 ]; then #Not stopped so presumably running
echo "Stopping home assistant..."
ha core stop >&/dev/null
if [ $? -ne 0 ]; then
echo "Error: Couldn't stop home assistant..."
exit 4
fi
fi
cd $STORAGE
#Get metadata for state and statistics tables from $DATABASE
declare -A METADATA #Associative array for state metadata id: entity_id -> metadata_id
while IFS='|' read meta entity; do
METADATA["$entity"]="$meta"
done <<< $(sqlite3 $DATABASE "SELECT metadata_id, entity_id FROM states_meta")
#for id in ${!METADATA[@]}; do echo "$id ---> ${METADATA[$id]}"; done #JJKDEBUG
declare -A METADATA_STAT #Associative array for statistic metadata id: statistic_id -> id
while IFS='|' read id statistic; do
METADATA_STAT["$statistic"]="$id"
done <<< $(sqlite3 $DATABASE "SELECT id, statistic_id FROM statistics_meta")
#for id in ${!METADATA_STAT[@]}; do echo "$id ---> ${METADATA_STAT[$id]}"; done #JJKDEBUG
## Get Device id & Name
if [ -n "$ALL" ]; then #Find device containing entity if '-e' or '-E' together with '-A' specified
if [ -n "$ENTID" ]; then
DEVID=$(cat $ENTREGISTRY | jq -M '.data.entities.[], .data.deleted_entities.[] | select(.id == "'$ENTID'") | .device_id' | tr -d \" 2>/dev/null)
elif [ -n "$ENTNAME" ]; then
DEVID=$(cat $ENTREGISTRY | jq -M '.data.entities.[], .data.deleted_entities.[] | select(.entity_id == "'$ENTNAME'") | .device_id' | tr -d \" 2>/dev/null)
fi
if [ -z "$DEVID" ]; then
echo "Error: Can't find device id correspinding to entity '${ENTID:-$ENTNAME}' in '$DEVREGISTRY'" >&2
exit 5
fi
unset ENTID ENTNAME
fi
if [ -n "$DEVID" ]; then #Look up by DEVID
{ read -r DEVNAME; read -r DEVUSERNAME; } \
<<< $(cat $DEVREGISTRY | jq -M '.data.devices.[], .data.deleted_devices.[] | select(.id == "'$DEVID'") | .name, .name_by_user' | tr -d \" 2>/dev/null)
if [ -z "$DEVNAME" ]; then
echo "Error: Can't find device id '$DEVID' in '$DEVREGISTRY'" >&2
exit 6
fi
elif [ -n "$DEVNAME" ]; then #Look up by DEVNAME
{ read -r DEVID; read -r DEVUSERNAME; } \
<<< $(cat $DEVREGISTRY | jq -M '.data.devices.[], .data.deleted_devices.[] | select(.name == "'$DEVNAME'") | .id, .name_by_user' | tr -d \" 2>/dev/null)
if [ -z "$DEVID" ]; then
echo "Error: Can't find device name '$DEVNAME' in '$DEVREGISTRY'" >&2
exit 7
fi
fi
#Get Entity id's and associated info
if [ -n "$ENTID" ]; then
ENT_IDS=($ENTID)
if [ -z "$(cat $ENTREGISTRY | jq -M '.data.entities.[], .data.deleted_entities.[] | select(.id == "'$ENTID'")')" ]; then
echo "Error: Can't find '$ENTID' in '$ENTREGISTRY'" >&2
exit 8
fi
elif [ -n "$ENTNAME" ]; then
ENT_IDS=($(cat $ENTREGISTRY | jq -M '.data.entities.[], .data.deleted_entities.[] | select(.entity_id == "'$ENTNAME'") | .id' | tr -d \" 2>/dev/null))
if [ ${#ENT_IDS[@]} -eq 0 ]; then
echo "Error: Can't find any entities corresponding to '$ENTNAME' in '$ENTREGISTRY'" >&2
exit 9
fi
else #Device + Entities
echo -e "Device: Name: $DEVNAME [$DEVID]\tUser name: $DEVUSERNAME"
#Get Corresponding Entity id's
ENT_IDS=($(cat $ENTREGISTRY | jq -M '.data.entities.[], .data.deleted_entities.[] | select(.device_id == "'$DEVID'") | .id' | tr -d \" 2>/dev/null))
if [ ${#ENT_IDS[@]} -eq 0 ]; then
echo -n "Can't find any entities corresponding to '$DEVNAME' ($DEVID) in '$ENTREGISTRY'. Delete device anyway? (y/n) "
read -e ANSWER
echo
if [ "$ANSWER" != "y" ]; then
echo "Aborting..."
exit 10
fi
fi
fi
#for id in ${ENT_IDS[@]}; do echo "|$id|"; done #JJKDEBUG
[ ${#ENT_IDS[@]} -ne 0 ] && printf "\n%-32s %-7s %-7s %-20s %-20s %-20s\n" ID META_ID STAT_ID ENTITY_ID "(UNIQUE_ID)" "[ORIGINAL_NAME]"
for id in ${ENT_IDS[@]}; do
#Note first need to declare assoc arrays since index is hex and can be interpeted as numbered base for an indexed array
declare -A ENTIDNAMES[$id] ENTUIDS[$id] ENTORIGNAMES[$id] METAIDS[$id] METASTATIDS[$id]
{ read -r entity_id; read -r unique_id; read -r original_name; } \
<<< $(cat $ENTREGISTRY | jq -M '.data.entities.[], .data.deleted_entities.[] | select(.id == "'$id'") | .entity_id, .unique_id, .original_name' | tr -d \")
ENTIDNAMES[$id]=$entity_id
ENTUIDS[$id]=$unique_id
ENTORIGNAMES[$id]=$original_name
METAIDS[$id]=${METADATA["$entity_id"]}
[ -z "${METAIDS[$id]}" ] && METAIDS[$id]='-'
METASTATIDS[$id]=${METADATA_STAT["$entity_id"]}
[ -z "${METASTATIDS[$id]}" ] && METASTATIDS[$id]='-'
printf "%s %-7s %-7s %s (%s) [%s]\n" "$id" "${METAIDS[$id]}" "${METASTATIDS[$id]}" "${ENTIDNAMES[$id]}" "${ENTUIDS[$id]}" "${ENTORIGNAMES[$id]}"
[ "${METAIDS[$id]}" = '-' ] && echo -e " WARNING: No 'metadata_id' stored in database for $entity_id\n"
done
echo -ne "\nAre you sure you want to COMPLETELY delete the above ${DEVID:+device &} ${#ENT_IDS[@]} entities (including all associated db data)?(y/n) "
read -e ANSWER
echo
if [ "$ANSWER" != "y" ]; then
echo "Aborting..."
exit 11
fi
#===============================================================================
#### Delete entries in '.storage' registries
DATE="$(date +%Y%m%d-%H%M%S)"
### Delete entry in $DEVREGISTRY (core.device_registry) if device
if [ -n "$DEVID" ]; then
cp -a $DEVREGISTRY{,-$DATE}
echo "...'$DEVREGISTRY' backed up to '${DEVREGISTRY}-$DATE'"
cat $DEVREGISTRY-$DATE | jq -M '.data |= del(.devices.[], .deleted_devices.[] | select(.id == "'$DEVID'"))' >| $DEVREGISTRY
if [ $? -ne 0 ]; then #Error - unwind & exit
echo "Error: Failed to delete *device* entries from '$DEVREGISTRY', unwinding..."
mv ${DEVREGISTRY}{-$DATE,}
exit 12
fi
echo " DELETED *device* entries from '$DEVREGISTRY'"
fi
if [ ${#ENT_IDS[@]} -eq 0 ];then #Device only so exit
print_exit
exit 0
fi
### Delete entries in $ENTREGISTRY (core.entity_registry)
cp -a $ENTREGISTRY{,-$DATE}
echo "...'$ENTREGISTRY' backed up to '${ENTREGISTRY}-$DATE'"
ENTJOINS=".entity_id == \"$(myjoin "\" or .entity_id == \"" ${ENTIDNAMES[@]})\"" #Selection string for all entities
cat $ENTREGISTRY-$DATE | jq -M '.data |= del(.entities.[], .deleted_entities.[] | select('"$ENTJOINS"'))' >| $ENTREGISTRY
if [ $? -ne 0 ]; then #Error - unwind & exit
echo "Error: Failed to delete *entity* entries from '$ENTREGISTRY', unwinding..."
mv ${DEVREGISTRY}{-$DATE,}
mv ${ENTREGISTRY}{-$DATE,}
exit 13
fi
echo " DELETED *entity* entries from '$ENTREGISTRY'"
### Delete entries in $RESTOREREGISTRY (core.restore_state)
cp -a $RESTOREREGISTRY{,-$DATE}
echo "...'$RESTOREREGISTRY' backed up to '${RESTOREREGISTRY}-$DATE'"
ENTJOINS=".state.entity_id == \"$(myjoin "\" or .state.entity_id == \"" ${ENTIDNAMES[@]})\"" #Selection string for all entities
cat $RESTOREREGISTRY-$DATE | jq -M '.data |= del(.[] | select('"$ENTJOINS"'))' >| $RESTOREREGISTRY
if [ $? -ne 0 ]; then #Error - unwind & exit
echo "Error: Failed to delete *entity* entries from '$RESTOREREGISTRY', unwinding..."
mv ${DEVREGISTRY}{-$DATE,}
mv ${ENTREGISTRY}{-$DATE,}
mv ${RESTOREREGISTRY}{-$DATE,}
exit 14
fi
echo " DELETED *entity* entries from '$RESTOREREGISTRY'"
### Delete entries in $EXPOSEDENTITIES(homeassistant.exposed_entities)
cp -a $EXPOSEDENTITIES{,-$DATE}
echo "...'$EXPOSEDENTITIES' backed up to '${EXPOSEDENTITIES}-$DATE'"
ENTJOINS=".\"$(myjoin "\", .\"" ${ENTIDNAMES[@]})\"" #Selection string for all entities
cat $EXPOSEDENTITIES-$DATE | jq -M '.data |= del(.exposed_entities | select('"$ENTJOINS"'))' >| ${EXPOSEDENTITIES}
if [ $? -ne 0 ]; then #Error - unwind & exit
echo "Error: Failed to delete *entity* entries from '$EXPOSEDENTITIES', unwinding..."
mv ${DEVREGISTRY}{-$DATE,}
mv ${ENTREGISTRY}{-$DATE,}
mv ${RESTOREREGISTRY}{-$DATE,}
mv ${EXPOSEDENTITIES}{-$DATE,}
exit 15
fi
echo " DELETED *entity* entries from '$EXPOSEDENTITIES'"
#===============================================================================
#### Delete entries in 'home-assistant_v2.db'
cp -a $DATABASE{,-$DATE}
echo "...'$DATABASE' backed up to '$DATABASE-$DATE'"
#Needed later (NOTE: 'comm' needs lexicographical sort)
ATTRIBUTES_USED_ORIG="$(sqlite3 $DATABASE "SELECT DISTINCT attributes_id FROM states ORDER BY CAST(attributes_id AS text)")"
if ! [[ $(IFS=; echo "${METAIDS[*]}") =~ ^-*$ ]]; then ## Consists only of '-'
METAIDJOINS="metadata_id = '$(myjoin "' OR metadata_id = '" ${METAIDS[@]})'"
NUMDELS=$(sqlite3 $DATABASE "DELETE FROM states_meta WHERE $METAIDJOINS; SELECT changes()")
echo " DELETED $NUMDELS entries from 'states_meta' table"
NUMDELS=$(sqlite3 $DATABASE "DELETE FROM states WHERE $METAIDJOINS; SELECT changes()")
echo " DELETED $NUMDELS entries from 'states' table"
fi
if ! [[ $(IFS=; echo "${METASTATIDS[*]}") =~ ^-*$ ]]; then ## Consists only of '-'
IDSTATJOINS="id = '$(myjoin "' OR id = '" ${METASTATIDS[@]})'"
NUMDELS=$(sqlite3 $DATABASE "DELETE FROM statistics_meta WHERE $IDSTATJOINS; SELECT changes()")
echo " DELETED $NUMDELS entries from 'statistics_meta' table"
METAIDSTATJOINS="metadata_id = '$(myjoin "' OR metadata_id = '" ${METASTATIDS[@]})'"
NUMDELS=$(sqlite3 $DATABASE "DELETE FROM statistics WHERE $METAIDSTATJOINS; SELECT changes()")
echo " DELETED $NUMDELS entries from 'statistics' table"
NUMDELS=$(sqlite3 $DATABASE "DELETE FROM statistics_short_term WHERE $METAIDSTATJOINS; SELECT changes()")
echo " DELETED $NUMDELS entries from 'statistics_short_term' table"
fi
#===============================================================================
#### Delete orphaned entries in 'states_attributes' database
#NOTE: 'comm' needs lexicographical sort
ATTRIBUTES_USED="$(sqlite3 $DATABASE "SELECT DISTINCT attributes_id FROM states ORDER BY CAST(attributes_id AS text)")"
ATTRIBUTES_ALL="$(sqlite3 $DATABASE "SELECT attributes_id FROM state_attributes ORDER BY CAST(attributes_id AS text)")"
NEWLY_ORPHANED=($(comm -2 -3 <(echo "$ATTRIBUTES_USED_ORIG") <(echo "$ATTRIBUTES_USED")))
ALL_ORPHANED=($(comm -2 -3 <(echo "$ATTRIBUTES_ALL") <(echo "$ATTRIBUTES_USED")))
MISSING=($(comm -1 -3 <(echo "$ATTRIBUTES_ALL") <(echo "$ATTRIBUTES_USED")))
echo "There are '${#NEWLY_ORPHANED[@]}' NEWLY orphaned attributes and '${#ALL_ORPHANED[@]}' TOTAL orphaned attributes in 'state_attributes' table"
echo "(Also, there are ${#MISSING[@]} attributes that are referenced in 'states' but not found in 'state_attributes table)"
if [ ${#ALL_ORPHANED} -gt 0 ]; then
echo -ne "\nDelete orphaned attributes from 'state_attributes' table?([N]ewly/[T]otal/[A]bort) "
read -e ANSWER
echo
case $ANSWER in
N) DELATTRS="$(myjoin ',' "${NEWLY_ORPHANED[@]}")"
;;
T) DELATTRS="$(myjoin ',' "${ALL_ORPHANED[@]}")"
;;
*) DELATTRS=""
;;
esac
NUMDELS=$(sqlite3 $DATABASE "DELETE FROM state_attributes WHERE attributes_id IN ($DELATTRS); SELECT changes()")
echo " DELETED $NUMDELS entries from 'state_attributes' table"
fi
#===============================================================================
echo -e "\n***If device/entities are MQTT integrations, don't forget to unpublish the entities!"
print_exit