synthetic_home.tool.export_inventory
Export an inventory from a Home Assistant instance.
You can create a synthetic home inventory copied from an existing home assistant instance. You need to create an access token and export an inventory like this:
$ HASS_URL="http://home-assistant.home-assistant:8123"
$ API_TOKEN="XXXXXXXXXXX"
$ synthetic-home --debug export_inventory "${HASS_URL}" "${API_TOKEN}" > inventory.yaml
1"""Export an inventory from a Home Assistant instance. 2 3You can create a synthetic home inventory copied from an existing home assistant 4instance. You need to create an access token and export an inventory like this: 5 6```bash 7$ HASS_URL="http://home-assistant.home-assistant:8123" 8$ API_TOKEN="XXXXXXXXXXX" 9$ synthetic-home --debug export_inventory "${HASS_URL}" "${API_TOKEN}" > inventory.yaml 10``` 11""" 12 13import argparse 14import logging 15import slugify 16from typing import Any 17 18import aiohttp 19 20from synthetic_home import inventory, common 21 22_LOGGER = logging.getLogger(__name__) 23 24AREA_REGISTRY_LIST = "config/area_registry/list" 25DEVICE_REGISTRY_LIST = "config/device_registry/list" 26ENTITY_REGISTRY_LIST = "config/entity_registry/list" 27GET_STATES = "get_states" 28GET_CONFIG = "get_config" 29 30DOMAINS = { 31 "binary_sensor", 32 # TODO: Support calendar 33 # "calendar", 34 "camera", 35 "climate", 36 "cover", 37 "fan", 38 # TODO: Support event 39 # "event", 40 "light", 41 "lock", 42 "media_player", 43 # TODO: Support number, person, remote, select 44 # "number", 45 # "person", 46 # "remote", 47 # "select", 48 "sensor", 49 "switch", 50 "todo", 51 "vacuum", 52 "weather", 53 # TODO: Support zone 54 # "zone", 55} 56 57STRIP_ATTRIBUTES = { 58 "friendly_name", 59 "icon", 60} 61 62 63def create_arguments(args: argparse.ArgumentParser) -> None: 64 """Get parsed passed in arguments.""" 65 args.add_argument( 66 "homeassistant_url", 67 type=str, 68 help="Specifies url to the home assistant instance.", 69 ) 70 args.add_argument( 71 "auth_token", 72 type=str, 73 help="Specifies home assistant API token.", 74 ) 75 76 77class Counter: 78 def __init__(self) -> None: 79 """Counter.""" 80 self._value = 0 81 82 def increment(self) -> int: 83 """Return next value.""" 84 self._value += 1 85 return self._value 86 87 88next_id = Counter() 89 90 91async def auth_login(ws: aiohttp.ClientWebSocketResponse, auth_token: str) -> None: 92 """Login to the websocket.""" 93 auth_resp = await ws.receive_json() 94 assert auth_resp["type"] == "auth_required" 95 96 await ws.send_json({"type": "auth", "access_token": auth_token}) 97 98 auth_ok = await ws.receive_json() 99 assert auth_ok["type"] == "auth_ok" 100 101 102async def fetch_config(ws: aiohttp.ClientWebSocketResponse) -> dict[str, Any]: 103 """Fetch areas from websocket.""" 104 await ws.send_json({"id": next_id.increment(), "type": GET_CONFIG}) 105 data = await ws.receive_json() 106 _LOGGER.info(data["result"]) 107 assert data["result"] 108 return data["result"] # type: ignore[no-any-return] 109 110 111async def fetch_areas(ws: aiohttp.ClientWebSocketResponse) -> dict[str, inventory.Area]: 112 """Fetch areas from websocket.""" 113 await ws.send_json({"id": next_id.increment(), "type": AREA_REGISTRY_LIST}) 114 data = await ws.receive_json() 115 areas = { 116 area["area_id"]: inventory.Area( 117 id=slugify.slugify(area["name"], separator="_"), 118 name=area["name"], 119 floor=area["floor_id"], 120 ) 121 for area in data["result"] 122 } 123 return areas 124 125 126async def fetch_devices( 127 ws: aiohttp.ClientWebSocketResponse, areas: dict[str, inventory.Area] 128) -> dict[str, inventory.Device]: 129 """Fetch areas from websocket.""" 130 131 await ws.send_json({"id": next_id.increment(), "type": DEVICE_REGISTRY_LIST}) 132 data = await ws.receive_json() 133 134 devices = {} 135 for device in data["result"]: 136 if device.get("disabled_by") is not None: 137 continue 138 inv_device = inventory.Device( 139 name=device["name"], 140 id=slugify.slugify(device["name"], separator="_"), 141 ) 142 if any(device.get(n) for n in ("model", "manufacturer", "sw_version")): 143 device_info = common.DeviceInfo() 144 if model := device.get("model"): 145 device_info.model = model 146 if manufacturer := device.get("manufacturer"): 147 device_info.manufacturer = manufacturer 148 if sw_version := device.get("sw_version"): 149 device_info.model = sw_version 150 inv_device.info = device_info 151 if area_id := device.get("area_id"): 152 inv_device.area = areas[area_id].id 153 devices[device["id"]] = inv_device 154 return devices 155 156 157async def fetch_entities( 158 ws: aiohttp.ClientWebSocketResponse, 159 areas: dict[str, inventory.Area], 160 devices: dict[str, inventory.Device], 161) -> dict[str, inventory.Entity]: 162 """Fetch devices from websocket.""" 163 await ws.send_json({"id": next_id.increment(), "type": ENTITY_REGISTRY_LIST}) 164 data = await ws.receive_json() 165 166 entities = {} 167 for entity in data["result"]: 168 if entity.get("disabled_by") is not None: 169 continue 170 if entity.get("hidden_by") is not None: 171 continue 172 if entity.get("entity_category") in ("diagnostic", "config"): 173 continue 174 entity_id = entity["entity_id"] 175 domain = entity_id.split(".", maxsplit=1)[0] 176 if domain not in DOMAINS: 177 _LOGGER.debug( 178 "Skipping entity with unsupported domain %s (%s)", 179 entity_id, 180 domain, 181 ) 182 continue 183 inv_entity = inventory.Entity( 184 name=entity["name"], 185 id=entity_id, 186 ) 187 if area_id := entity.get("area_id"): 188 inv_entity.area = areas[area_id].id 189 if device_id := entity.get("device_id"): 190 inv_entity.device = devices[device_id].id 191 entities[entity_id] = inv_entity 192 return entities 193 194 195async def build_states( 196 ws: aiohttp.ClientWebSocketResponse, 197 entities: dict[str, inventory.Entity], 198 unit_system: dict[str, str], 199) -> list[inventory.Entity]: 200 """Fetch states from websocket and update inventory.""" 201 202 await ws.send_json({"id": next_id.increment(), "type": GET_STATES}) 203 data = await ws.receive_json() 204 205 temperature_unit = unit_system["temperature"] 206 results = [] 207 for state in data["result"]: 208 entity_id = state["entity_id"] 209 if (inv_entity := entities.get(entity_id)) is None: 210 continue 211 entity_state = state["state"] 212 if entity_state in ("unavailable", "unknown"): 213 continue 214 inv_entity.state = entity_state 215 if (attributes := state.get("attributes")) is not None: 216 if friendly_name := attributes.get("friendly_name"): 217 inv_entity.name = friendly_name.strip() 218 inv_attributes = { 219 k: v 220 for k, v in attributes.items() 221 if (v is not None) and (k not in STRIP_ATTRIBUTES) 222 } 223 224 if entity_id.startswith("climate."): 225 inv_attributes["unit_of_measurement"] = temperature_unit 226 if inv_attributes: 227 inv_entity.attributes = inv_attributes 228 results.append(inv_entity) 229 return results 230 231 232async def run(args: argparse.Namespace) -> int: 233 url = args.homeassistant_url 234 auth_token = args.auth_token 235 236 async with aiohttp.ClientSession() as session: 237 area_url = f"{url}/api/websocket" 238 _LOGGER.info("Fetching areas from %s", url) 239 async with session.ws_connect(area_url) as ws: 240 await auth_login(ws, auth_token) 241 242 config = await fetch_config(ws) 243 unit_system = config["unit_system"] 244 areas = await fetch_areas(ws) 245 devices = await fetch_devices(ws, areas) 246 entities = await fetch_entities(ws, areas, devices) 247 248 required_device_ids = set({entity.device for entity in entities.values()}) 249 250 # Only include required devices for entities 251 inv = inventory.Inventory() 252 253 # Fetch state for all relevant entities 254 inv.entities = await build_states(ws, entities, unit_system) 255 inv.areas = list(areas.values()) 256 inv.devices = [ 257 device 258 for device in devices.values() 259 if device.id in required_device_ids 260 ] 261 262 print(inv.yaml()) 263 264 return 0
AREA_REGISTRY_LIST =
'config/area_registry/list'
DEVICE_REGISTRY_LIST =
'config/device_registry/list'
ENTITY_REGISTRY_LIST =
'config/entity_registry/list'
GET_STATES =
'get_states'
GET_CONFIG =
'get_config'
DOMAINS =
{'vacuum', 'binary_sensor', 'light', 'fan', 'lock', 'cover', 'media_player', 'sensor', 'weather', 'climate', 'switch', 'todo', 'camera'}
STRIP_ATTRIBUTES =
{'friendly_name', 'icon'}
def
create_arguments(args: argparse.ArgumentParser) -> None:
64def create_arguments(args: argparse.ArgumentParser) -> None: 65 """Get parsed passed in arguments.""" 66 args.add_argument( 67 "homeassistant_url", 68 type=str, 69 help="Specifies url to the home assistant instance.", 70 ) 71 args.add_argument( 72 "auth_token", 73 type=str, 74 help="Specifies home assistant API token.", 75 )
Get parsed passed in arguments.
class
Counter:
next_id =
<Counter object>
async def
auth_login(ws: aiohttp.client_ws.ClientWebSocketResponse, auth_token: str) -> None:
92async def auth_login(ws: aiohttp.ClientWebSocketResponse, auth_token: str) -> None: 93 """Login to the websocket.""" 94 auth_resp = await ws.receive_json() 95 assert auth_resp["type"] == "auth_required" 96 97 await ws.send_json({"type": "auth", "access_token": auth_token}) 98 99 auth_ok = await ws.receive_json() 100 assert auth_ok["type"] == "auth_ok"
Login to the websocket.
async def
fetch_config(ws: aiohttp.client_ws.ClientWebSocketResponse) -> dict[str, typing.Any]:
103async def fetch_config(ws: aiohttp.ClientWebSocketResponse) -> dict[str, Any]: 104 """Fetch areas from websocket.""" 105 await ws.send_json({"id": next_id.increment(), "type": GET_CONFIG}) 106 data = await ws.receive_json() 107 _LOGGER.info(data["result"]) 108 assert data["result"] 109 return data["result"] # type: ignore[no-any-return]
Fetch areas from websocket.
async def
fetch_areas( ws: aiohttp.client_ws.ClientWebSocketResponse) -> dict[str, synthetic_home.inventory.Area]:
112async def fetch_areas(ws: aiohttp.ClientWebSocketResponse) -> dict[str, inventory.Area]: 113 """Fetch areas from websocket.""" 114 await ws.send_json({"id": next_id.increment(), "type": AREA_REGISTRY_LIST}) 115 data = await ws.receive_json() 116 areas = { 117 area["area_id"]: inventory.Area( 118 id=slugify.slugify(area["name"], separator="_"), 119 name=area["name"], 120 floor=area["floor_id"], 121 ) 122 for area in data["result"] 123 } 124 return areas
Fetch areas from websocket.
async def
fetch_devices( ws: aiohttp.client_ws.ClientWebSocketResponse, areas: dict[str, synthetic_home.inventory.Area]) -> dict[str, synthetic_home.inventory.Device]:
127async def fetch_devices( 128 ws: aiohttp.ClientWebSocketResponse, areas: dict[str, inventory.Area] 129) -> dict[str, inventory.Device]: 130 """Fetch areas from websocket.""" 131 132 await ws.send_json({"id": next_id.increment(), "type": DEVICE_REGISTRY_LIST}) 133 data = await ws.receive_json() 134 135 devices = {} 136 for device in data["result"]: 137 if device.get("disabled_by") is not None: 138 continue 139 inv_device = inventory.Device( 140 name=device["name"], 141 id=slugify.slugify(device["name"], separator="_"), 142 ) 143 if any(device.get(n) for n in ("model", "manufacturer", "sw_version")): 144 device_info = common.DeviceInfo() 145 if model := device.get("model"): 146 device_info.model = model 147 if manufacturer := device.get("manufacturer"): 148 device_info.manufacturer = manufacturer 149 if sw_version := device.get("sw_version"): 150 device_info.model = sw_version 151 inv_device.info = device_info 152 if area_id := device.get("area_id"): 153 inv_device.area = areas[area_id].id 154 devices[device["id"]] = inv_device 155 return devices
Fetch areas from websocket.
async def
fetch_entities( ws: aiohttp.client_ws.ClientWebSocketResponse, areas: dict[str, synthetic_home.inventory.Area], devices: dict[str, synthetic_home.inventory.Device]) -> dict[str, synthetic_home.inventory.Entity]:
158async def fetch_entities( 159 ws: aiohttp.ClientWebSocketResponse, 160 areas: dict[str, inventory.Area], 161 devices: dict[str, inventory.Device], 162) -> dict[str, inventory.Entity]: 163 """Fetch devices from websocket.""" 164 await ws.send_json({"id": next_id.increment(), "type": ENTITY_REGISTRY_LIST}) 165 data = await ws.receive_json() 166 167 entities = {} 168 for entity in data["result"]: 169 if entity.get("disabled_by") is not None: 170 continue 171 if entity.get("hidden_by") is not None: 172 continue 173 if entity.get("entity_category") in ("diagnostic", "config"): 174 continue 175 entity_id = entity["entity_id"] 176 domain = entity_id.split(".", maxsplit=1)[0] 177 if domain not in DOMAINS: 178 _LOGGER.debug( 179 "Skipping entity with unsupported domain %s (%s)", 180 entity_id, 181 domain, 182 ) 183 continue 184 inv_entity = inventory.Entity( 185 name=entity["name"], 186 id=entity_id, 187 ) 188 if area_id := entity.get("area_id"): 189 inv_entity.area = areas[area_id].id 190 if device_id := entity.get("device_id"): 191 inv_entity.device = devices[device_id].id 192 entities[entity_id] = inv_entity 193 return entities
Fetch devices from websocket.
async def
build_states( ws: aiohttp.client_ws.ClientWebSocketResponse, entities: dict[str, synthetic_home.inventory.Entity], unit_system: dict[str, str]) -> list[synthetic_home.inventory.Entity]:
196async def build_states( 197 ws: aiohttp.ClientWebSocketResponse, 198 entities: dict[str, inventory.Entity], 199 unit_system: dict[str, str], 200) -> list[inventory.Entity]: 201 """Fetch states from websocket and update inventory.""" 202 203 await ws.send_json({"id": next_id.increment(), "type": GET_STATES}) 204 data = await ws.receive_json() 205 206 temperature_unit = unit_system["temperature"] 207 results = [] 208 for state in data["result"]: 209 entity_id = state["entity_id"] 210 if (inv_entity := entities.get(entity_id)) is None: 211 continue 212 entity_state = state["state"] 213 if entity_state in ("unavailable", "unknown"): 214 continue 215 inv_entity.state = entity_state 216 if (attributes := state.get("attributes")) is not None: 217 if friendly_name := attributes.get("friendly_name"): 218 inv_entity.name = friendly_name.strip() 219 inv_attributes = { 220 k: v 221 for k, v in attributes.items() 222 if (v is not None) and (k not in STRIP_ATTRIBUTES) 223 } 224 225 if entity_id.startswith("climate."): 226 inv_attributes["unit_of_measurement"] = temperature_unit 227 if inv_attributes: 228 inv_entity.attributes = inv_attributes 229 results.append(inv_entity) 230 return results
Fetch states from websocket and update inventory.
async def
run(args: argparse.Namespace) -> int:
233async def run(args: argparse.Namespace) -> int: 234 url = args.homeassistant_url 235 auth_token = args.auth_token 236 237 async with aiohttp.ClientSession() as session: 238 area_url = f"{url}/api/websocket" 239 _LOGGER.info("Fetching areas from %s", url) 240 async with session.ws_connect(area_url) as ws: 241 await auth_login(ws, auth_token) 242 243 config = await fetch_config(ws) 244 unit_system = config["unit_system"] 245 areas = await fetch_areas(ws) 246 devices = await fetch_devices(ws, areas) 247 entities = await fetch_entities(ws, areas, devices) 248 249 required_device_ids = set({entity.device for entity in entities.values()}) 250 251 # Only include required devices for entities 252 inv = inventory.Inventory() 253 254 # Fetch state for all relevant entities 255 inv.entities = await build_states(ws, entities, unit_system) 256 inv.areas = list(areas.values()) 257 inv.devices = [ 258 device 259 for device in devices.values() 260 if device.id in required_device_ids 261 ] 262 263 print(inv.yaml()) 264 265 return 0