@@ -57,42 +57,143 @@ async def async_setup_entry( # noqa: C901
5757 # Use the coordinator from hass.data that was created in __init__.py
5858 coordinator = hass .data [DOMAIN ]["coordinator" ]
5959
60- # Track existing extra field entities to detect new ones
61- existing_extra_fields = {} # key: (spool_id, field_key), value: entity
60+ # Track which spools / extra fields we've already materialised so we can
61+ # add brand-new spools and extras when the coordinator notices them
62+ # (#327: previously only extra fields were added dynamically; new spools
63+ # required a full integration reload).
64+ existing_spool_ids : set = set ()
65+ existing_extra_fields : dict = {} # key: (spool_id, field_key), value: entity
66+
67+ image_dir = hass .config .path (PUBLIC_IMAGE_PATH )
68+
69+ async def _build_entities_for_spool (spool , idx ):
70+ """Build the full sensor stack for a single spool."""
71+ entities : list = []
72+ image_url = await hass .async_add_executor_job (
73+ _generate_entity_picture , spool , image_dir
74+ )
75+ entities .append (Spool (hass , coordinator , spool , idx , config_entry , image_url ))
76+ entities .append (SpoolFlowRate (hass , coordinator , spool , config_entry ))
77+ entities .append (SpoolEstimatedRunOut (hass , coordinator , spool , config_entry ))
78+ entities .append (SpoolUsedWeight (hass , coordinator , spool , config_entry ))
79+ entities .append (SpoolRemainingLength (hass , coordinator , spool , config_entry ))
80+ entities .append (SpoolUsedLength (hass , coordinator , spool , config_entry ))
81+ entities .append (SpoolLocation (hass , coordinator , spool , config_entry ))
82+ entities .append (SpoolUsedPercentage (hass , coordinator , spool , config_entry ))
83+
84+ if spool .get ("registered" ):
85+ entities .append (SpoolRegistered (hass , coordinator , spool , config_entry ))
86+ if spool .get ("first_used" ):
87+ entities .append (SpoolFirstUsed (hass , coordinator , spool , config_entry ))
88+ if spool .get ("last_used" ):
89+ entities .append (SpoolLastUsed (hass , coordinator , spool , config_entry ))
90+ if spool .get ("price" ) is not None :
91+ entities .append (SpoolPrice (hass , coordinator , spool , config_entry ))
92+ if spool .get ("spool_weight" ) is not None :
93+ entities .append (SpoolWeight (hass , coordinator , spool , config_entry ))
94+ if spool .get ("lot_nr" ):
95+ entities .append (SpoolLotNumber (hass , coordinator , spool , config_entry ))
96+ if spool .get ("comment" ):
97+ entities .append (SpoolComment (hass , coordinator , spool , config_entry ))
98+
99+ filament = spool .get ("filament" , {})
100+ if filament .get ("density" ) is not None :
101+ entities .append (FilamentDensity (hass , coordinator , spool , config_entry ))
102+ if filament .get ("diameter" ) is not None :
103+ entities .append (FilamentDiameter (hass , coordinator , spool , config_entry ))
104+ if filament .get ("settings_extruder_temp" ) is not None :
105+ entities .append (FilamentExtruderTemp (hass , coordinator , spool , config_entry ))
106+ if filament .get ("settings_bed_temp" ) is not None :
107+ entities .append (FilamentBedTemp (hass , coordinator , spool , config_entry ))
108+ if filament .get ("article_number" ):
109+ entities .append (FilamentArticleNumber (hass , coordinator , spool , config_entry ))
110+
111+ entities .append (SpoolId (hass , coordinator , spool , config_entry ))
112+
113+ if filament .get ("name" ):
114+ entities .append (FilamentName (hass , coordinator , spool , config_entry ))
115+ if filament .get ("material" ):
116+ entities .append (FilamentMaterial (hass , coordinator , spool , config_entry ))
117+ if filament .get ("color_hex" ):
118+ filament_image_url = await hass .async_add_executor_job (
119+ _generate_filament_entity_picture , filament , image_dir
120+ )
121+ entities .append (
122+ FilamentColorHex (
123+ hass , coordinator , spool , config_entry , filament_image_url
124+ )
125+ )
126+ if filament .get ("vendor" , {}).get ("name" ):
127+ entities .append (VendorName (hass , coordinator , spool , config_entry ))
128+ if filament .get ("weight" ) is not None :
129+ entities .append (FilamentWeight (hass , coordinator , spool , config_entry ))
130+
131+ for field_key in spool .get ("extra" , {}):
132+ extra_sensor = SpoolExtraField (
133+ hass , coordinator , spool , config_entry , field_key
134+ )
135+ entities .append (extra_sensor )
136+ existing_extra_fields [(spool ["id" ], field_key )] = extra_sensor
137+
138+ existing_spool_ids .add (spool ["id" ])
139+ return entities
140+
141+ async def _async_add_new_spools (new_spools ):
142+ """Build & register sensors for spools the coordinator just discovered."""
143+ new_entities : list = []
144+ # Use a stable index continuation for the picture filename.
145+ base_idx = len (existing_spool_ids )
146+ for offset , spool in enumerate (new_spools ):
147+ _LOGGER .info (
148+ "Dynamically adding sensors for new spool %s" , spool .get ("id" )
149+ )
150+ new_entities .extend (
151+ await _build_entities_for_spool (spool , base_idx + offset )
152+ )
153+ if new_entities :
154+ async_add_entities (new_entities )
62155
63156 @callback
64- def add_extra_field_entities ():
65- """Add new extra field entities when they appear in coordinator data ."""
157+ def add_dynamic_entities ():
158+ """Add new spools and new extra- field sensors as they appear."""
66159 if not coordinator .data :
67160 return
68161
69- new_entities = []
70162 spools = coordinator .data .get ("spools" , [])
71163
164+ # New spools (#327): full sensor stack, async because of image gen.
165+ new_spools = [
166+ s for s in spools if s .get ("id" ) not in existing_spool_ids
167+ ]
168+ if new_spools :
169+ hass .async_create_task (_async_add_new_spools (new_spools ))
170+
171+ # New extra fields on already-known spools.
172+ new_entities = []
72173 for spool in spools :
73174 spool_id = spool .get ("id" )
74- extra_data = spool .get ("extra" , {})
75-
76- for field_key in extra_data :
175+ if spool_id not in existing_spool_ids :
176+ # Will be handled by _async_add_new_spools above.
177+ continue
178+ for field_key in spool .get ("extra" , {}):
77179 entity_key = (spool_id , field_key )
78-
79- # Only create if it doesn't exist yet
80180 if entity_key not in existing_extra_fields :
81181 extra_field_sensor = SpoolExtraField (
82182 hass , coordinator , spool , config_entry , field_key
83183 )
84184 new_entities .append (extra_field_sensor )
85185 existing_extra_fields [entity_key ] = extra_field_sensor
86186 _LOGGER .info (
87- f"Dynamically adding new extra field sensor for spool { spool_id } : { field_key } "
187+ "Dynamically adding new extra field sensor for spool %s: %s" ,
188+ spool_id ,
189+ field_key ,
88190 )
89191
90192 if new_entities :
91193 async_add_entities (new_entities )
92194
93195 if coordinator .data :
94196 all_entities = []
95- image_dir = hass .config .path (PUBLIC_IMAGE_PATH )
96197
97198 # Create spool entities
98199 spool_data = coordinator .data .get ("spools" , [])
@@ -104,6 +205,7 @@ def add_extra_field_entities():
104205 hass , coordinator , spool , idx , config_entry , image_url
105206 )
106207 all_entities .append (spool_device )
208+ existing_spool_ids .add (spool ["id" ])
107209
108210 # Create flow rate sensor for this spool
109211 flow_rate_sensor = SpoolFlowRate (
@@ -312,8 +414,9 @@ def add_extra_field_entities():
312414
313415 async_add_entities (all_entities )
314416
315- # Register listener for coordinator updates to add new extra fields
316- coordinator .async_add_listener (add_extra_field_entities )
417+ # Register listener for coordinator updates so new spools (#327) and new
418+ # extra fields are added without requiring an integration reload.
419+ coordinator .async_add_listener (add_dynamic_entities )
317420
318421
319422def _generate_entity_picture (spool_data , image_dir ):
0 commit comments