@@ -143,7 +143,11 @@ async def get_entities_by_area(
143143 self , area_query : str , group_by_domain : bool = True
144144 ) -> dict [str , Any ]:
145145 """
146- Get entities grouped by area/room with fuzzy matching.
146+ Get entities grouped by area/room using the HA registries for accurate area resolution.
147+
148+ Uses entity registry, device registry, and area registry to determine
149+ which area each entity belongs to. Fuzzy matches the query against
150+ area names/IDs to find the target area(s).
147151
148152 Args:
149153 area_query: Area/room name to search for
@@ -153,55 +157,163 @@ async def get_entities_by_area(
153157 Dictionary with area-grouped entities
154158 """
155159 try :
156- # Get all entities
157- entities = await self .client .get_states ()
160+ # Fetch all registries and states in parallel
161+ entities_task = self .client .get_states ()
162+ area_registry_task = self .client .send_websocket_message (
163+ {"type" : "config/area_registry/list" }
164+ )
165+ entity_registry_task = self .client .send_websocket_message (
166+ {"type" : "config/entity_registry/list" }
167+ )
168+ device_registry_task = self .client .send_websocket_message (
169+ {"type" : "config/device_registry/list" }
170+ )
158171
159- # Search by area
160- area_matches = self .fuzzy_searcher .search_by_area (entities , area_query )
172+ results = await asyncio .gather (
173+ entities_task ,
174+ area_registry_task ,
175+ entity_registry_task ,
176+ device_registry_task ,
177+ return_exceptions = True ,
178+ )
161179
162- # Format results
163- formatted_areas = {}
180+ entities = results [0 ] if not isinstance (results [0 ], Exception ) else []
181+
182+ # Parse area registry: area_id -> area info
183+ area_registry : dict [str , dict [str , Any ]] = {}
184+ if isinstance (results [1 ], dict ) and results [1 ].get ("success" ):
185+ for area in results [1 ].get ("result" , []):
186+ area_id = area .get ("area_id" , "" )
187+ if area_id :
188+ area_registry [area_id ] = area
189+
190+ # Parse entity registry: entity_id -> {area_id, device_id}
191+ entity_reg_map : dict [str , dict [str , str | None ]] = {}
192+ if isinstance (results [2 ], dict ) and results [2 ].get ("success" ):
193+ for entry in results [2 ].get ("result" , []):
194+ entity_id = entry .get ("entity_id" )
195+ if entity_id :
196+ entity_reg_map [entity_id ] = {
197+ "area_id" : entry .get ("area_id" ),
198+ "device_id" : entry .get ("device_id" ),
199+ }
200+
201+ # Parse device registry: device_id -> area_id
202+ device_area_map : dict [str , str | None ] = {}
203+ if isinstance (results [3 ], dict ) and results [3 ].get ("success" ):
204+ for device in results [3 ].get ("result" , []):
205+ device_id = device .get ("id" , "" )
206+ if device_id :
207+ device_area_map [device_id ] = device .get ("area_id" )
208+
209+ # Fuzzy match area_query against known area names and IDs
210+ area_query_lower = area_query .lower ().strip ()
211+ matched_area_ids : set [str ] = set ()
212+
213+ for area_id , area_info in area_registry .items ():
214+ area_name = area_info .get ("name" , "" )
215+ # Exact match on area_id or name (case-insensitive)
216+ if area_query_lower == area_id .lower () or area_query_lower == area_name .lower ():
217+ matched_area_ids .add (area_id )
218+ continue
219+ # Fuzzy match on area name
220+ name_score = calculate_partial_ratio (area_query_lower , area_name .lower ())
221+ id_score = calculate_partial_ratio (area_query_lower , area_id .lower ())
222+ best_score = max (name_score , id_score )
223+ if best_score >= 80 :
224+ matched_area_ids .add (area_id )
225+
226+ if not matched_area_ids :
227+ return {
228+ "area_query" : area_query ,
229+ "total_areas_found" : 0 ,
230+ "total_entities" : 0 ,
231+ "areas" : {},
232+ "search_metadata" : {
233+ "grouped_by_domain" : group_by_domain ,
234+ "area_inference_method" : "registry_lookup" ,
235+ "available_areas" : [
236+ {"area_id" : aid , "name" : ainfo .get ("name" , aid )}
237+ for aid , ainfo in area_registry .items ()
238+ ],
239+ },
240+ "usage_tips" : [
241+ "No areas matched your query. See available_areas for valid area names." ,
242+ "Try exact area names from Home Assistant" ,
243+ "Use ha_list_areas() to see all areas" ,
244+ ],
245+ }
246+
247+ # Build entity_id -> resolved area_id mapping
248+ # Priority: entity direct area_id > device area_id
249+ entity_area_resolved : dict [str , str ] = {}
250+ for entity_id , reg_info in entity_reg_map .items ():
251+ area_id = reg_info .get ("area_id" )
252+ if not area_id and reg_info .get ("device_id" ):
253+ area_id = device_area_map .get (reg_info ["device_id" ])
254+ if area_id :
255+ entity_area_resolved [entity_id ] = area_id
256+
257+ # Build state lookup for entity details
258+ state_map : dict [str , dict [str , Any ]] = {}
259+ for entity in entities :
260+ eid = entity .get ("entity_id" , "" )
261+ if eid :
262+ state_map [eid ] = entity
263+
264+ # Collect entities belonging to matched areas
265+ formatted_areas : dict [str , dict [str , Any ]] = {}
164266 total_entities = 0
165267
166- for area_name , area_entities in area_matches .items ():
167- area_data = {
268+ for area_id in matched_area_ids :
269+ area_info = area_registry .get (area_id , {})
270+ area_name = area_info .get ("name" , area_id )
271+
272+ # Find all entities in this area
273+ area_entities = [
274+ entity_id
275+ for entity_id , resolved_area in entity_area_resolved .items ()
276+ if resolved_area == area_id
277+ ]
278+
279+ area_data : dict [str , Any ] = {
168280 "area_name" : area_name ,
281+ "area_id" : area_id ,
169282 "entity_count" : len (area_entities ),
170283 "entities" : {},
171284 }
172285
173286 if group_by_domain :
174- # Group by domain
175287 domains : dict [str , list [dict [str , Any ]]] = {}
176- for entity in area_entities :
177- domain = entity ["entity_id" ].split ("." )[0 ]
288+ for entity_id in area_entities :
289+ domain = entity_id .split ("." )[0 ]
290+ state_info = state_map .get (entity_id , {})
178291 if domain not in domains :
179292 domains [domain ] = []
180293 domains [domain ].append (
181294 {
182- "entity_id" : entity [ " entity_id" ] ,
183- "friendly_name" : entity .get ("attributes" , {}).get (
184- "friendly_name" , entity [ " entity_id" ]
295+ "entity_id" : entity_id ,
296+ "friendly_name" : state_info .get ("attributes" , {}).get (
297+ "friendly_name" , entity_id
185298 ),
186- "state" : entity .get ("state" , "unknown" ),
299+ "state" : state_info .get ("state" , "unknown" ),
187300 }
188301 )
189302 area_data ["entities" ] = domains
190303 else :
191- # Flat list
192304 area_data ["entities" ] = [
193305 {
194- "entity_id" : entity [ " entity_id" ] ,
195- "friendly_name" : entity .get ("attributes" , {}). get (
196- "friendly_name " , entity [ "entity_id" ]
197- ),
198- "domain" : entity [ " entity_id" ] .split ("." )[0 ],
199- "state" : entity .get ("state" , "unknown" ),
306+ "entity_id" : entity_id ,
307+ "friendly_name" : ( state_info := state_map .get (entity_id , {}))
308+ . get ( "attributes " , {})
309+ . get ( "friendly_name" , entity_id ),
310+ "domain" : entity_id .split ("." )[0 ],
311+ "state" : state_info .get ("state" , "unknown" ),
200312 }
201- for entity in area_entities
313+ for entity_id in area_entities
202314 ]
203315
204- formatted_areas [area_name ] = area_data
316+ formatted_areas [area_id ] = area_data
205317 total_entities += len (area_entities )
206318
207319 return {
@@ -211,13 +323,12 @@ async def get_entities_by_area(
211323 "areas" : formatted_areas ,
212324 "search_metadata" : {
213325 "grouped_by_domain" : group_by_domain ,
214- "area_inference_method" : "fuzzy_name_matching " ,
326+ "area_inference_method" : "registry_lookup " ,
215327 },
216328 "usage_tips" : [
217329 "Try room names: 'salon', 'chambre', 'cuisine'" ,
218330 "English names: 'living', 'bedroom', 'kitchen'" ,
219- "Partial matches: 'bed' finds 'bedroom' entities" ,
220- "Use get_all_states to see all area_id attributes" ,
331+ "Use ha_list_areas() to see all available areas" ,
221332 ],
222333 }
223334
0 commit comments