1515 EVENT_BATTERY_STATUS ,
1616 EVENT_CHANGE_MODE ,
1717 EVENT_DEVICE_IDLE ,
18+ EVENT_FAULT ,
1819 EVENT_FS_ALARM ,
1920 EVENT_RAIN_DELAY ,
2021 EVENT_SET_MANUAL_PRESET_TIME ,
@@ -51,6 +52,7 @@ def __init__(
5152 )
5253 self .client = client
5354 self .entry = entry
55+ self .gateway_to_bridge : dict [str , str ] = {}
5456
5557 async def _async_update_data (self ) -> dict [str , Any ]:
5658 """Fetch data from API (periodic polling)."""
@@ -260,11 +262,30 @@ async def async_handle_device_event(self, event_data: dict[str, Any]) -> None:
260262 device_data ["status" ] = {}
261263 device_data ["status" ]["rain_delay" ] = event_data .get ("delay" , 0 )
262264
265+ elif event == EVENT_FAULT :
266+ # Update station fault information
267+ if "status" not in device_data :
268+ device_data ["status" ] = {}
269+ device_data ["status" ]["station_faults" ] = event_data .get (
270+ "station_faults" , []
271+ )
272+
263273 elif event == EVENT_FS_ALARM :
264- # Update flood sensor status
274+ # Update flood sensor status with only meaningful flood sensor fields.
275+ # Avoid a blind merge that could overwrite unrelated device-level keys.
265276 if "status" not in device_data :
266277 device_data ["status" ] = {}
267- device_data ["status" ].update (event_data )
278+ for key in (
279+ "flood_alarm_status" ,
280+ "temp_alarm_status" ,
281+ "temp_f" ,
282+ "rssi" ,
283+ "last_flood_alarm_at" ,
284+ "last_temp_alarm_at" ,
285+ "status_updated_at" ,
286+ ):
287+ if key in event_data :
288+ device_data ["status" ][key ] = event_data [key ]
268289
269290 elif event == EVENT_SET_MANUAL_PRESET_TIME :
270291 # Update manual preset runtime
@@ -275,20 +296,81 @@ async def async_handle_device_event(self, event_data: dict[str, Any]) -> None:
275296 # Notify all listening entities
276297 self .async_set_updated_data (self .data )
277298
299+ def _set_zones_smart_watering (
300+ self , device_id : str | None , * , enabled : bool
301+ ) -> None :
302+ """Update smart_watering_enabled on all zones of a device."""
303+ if not device_id :
304+ return
305+ device_data = self .data .get ("devices" , {}).get (device_id , {})
306+ device = device_data .get ("device" , {})
307+ for zone in device .get ("zones" , []):
308+ zone ["smart_watering_enabled" ] = enabled
309+
310+ @staticmethod
311+ def _get_program_id (event_data : dict [str , Any ]) -> str | None :
312+ """Extract program ID from event data."""
313+ program_id = event_data .get ("program_id" )
314+ if not program_id :
315+ program = event_data .get ("program" , {})
316+ program_id = program .get ("id" ) if isinstance (program , dict ) else None
317+ return program_id
318+
278319 async def async_handle_program_event (self , event_data : dict [str , Any ]) -> None :
279320 """Handle WebSocket program events."""
280321 if not self .data :
281322 _LOGGER .debug ("Coordinator data not initialized, ignoring event" )
282323 return
283324
284- program_id = event_data .get ("program_id" )
325+ lifecycle_phase = event_data .get ("lifecycle_phase" )
326+ program_id = self ._get_program_id (event_data )
285327
286328 if not program_id :
287- # Some program events have program in a different structure
288- program = event_data .get ("program" , {})
289- program_id = program .get ("id" ) if isinstance (program , dict ) else None
329+ _LOGGER .debug ("No program_id found in event, ignoring" )
330+ return
290331
291- if not program_id or program_id not in self .data ["programs" ]:
332+ # Handle program creation
333+ if lifecycle_phase == "create" :
334+ program_data = event_data .get ("program" )
335+ if isinstance (program_data , dict ):
336+ _LOGGER .debug ("Adding new program %s to coordinator data" , program_id )
337+ self .data ["programs" ][program_id ] = program_data
338+ if program_data .get ("is_smart_program" ):
339+ # Smart program created means smart watering was enabled
340+ device_id = event_data .get ("device_id" )
341+ self ._set_zones_smart_watering (device_id , enabled = True )
342+ else :
343+ self .hass .bus .async_fire (
344+ "bhyve_program_created" ,
345+ {"program_id" : program_id , "program" : program_data },
346+ )
347+ self .async_set_updated_data (self .data )
348+ return
349+
350+ # Handle program deletion (smart programs use "destroy" but should
351+ # be kept as entities since they represent a toggle, not a removal)
352+ if lifecycle_phase in ("delete" , "destroy" ):
353+ is_smart = (
354+ self .data ["programs" ].get (program_id , {}).get ("is_smart_program" , False )
355+ )
356+ if not is_smart and program_id in self .data ["programs" ]:
357+ _LOGGER .debug ("Removing program %s from coordinator data" , program_id )
358+ del self .data ["programs" ][program_id ]
359+ self .hass .bus .async_fire (
360+ "bhyve_program_deleted" ,
361+ {"program_id" : program_id },
362+ )
363+ self .async_set_updated_data (self .data )
364+ return
365+ if is_smart :
366+ # Smart program destroy means smart watering was disabled.
367+ # Update zone data so smart watering switches reflect the change.
368+ device_id = event_data .get ("device_id" )
369+ self ._set_zones_smart_watering (device_id , enabled = False )
370+ # Fall through to update the program data
371+
372+ # For update events, check if program exists
373+ if program_id not in self .data ["programs" ]:
292374 _LOGGER .debug (
293375 "Program %s not found in coordinator data, ignoring event" ,
294376 program_id ,
0 commit comments