@@ -430,14 +430,102 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry) -> bool:
430430 return True
431431
432432
433+ class ServiceCallData :
434+ """Store service call data and set default values"""
435+
436+ def __init__ (self , data_call ):
437+ # This is the config entry id
438+ self .provider = str (data_call .data .get (PROVIDER ))
439+ # If not set, the conf_default_model will be set in providers.py
440+ self .model = data_call .data .get (MODEL )
441+ self .message = str (data_call .data .get (MESSAGE , "" )[0 :2000 ])
442+ self .store_in_timeline = data_call .data .get (STORE_IN_TIMELINE , False )
443+ self .use_memory = data_call .data .get (USE_MEMORY , False )
444+ self .image_paths = (
445+ data_call .data .get (IMAGE_FILE , "" ).split ("\n " )
446+ if data_call .data .get (IMAGE_FILE )
447+ else None
448+ )
449+ self .image_entities = data_call .data .get (IMAGE_ENTITY )
450+ self .video_paths = (
451+ data_call .data .get (VIDEO_FILE , "" ).split ("\n " )
452+ if data_call .data .get (VIDEO_FILE )
453+ else None
454+ )
455+ self .event_id = (
456+ data_call .data .get (EVENT_ID , "" ).split ("\n " )
457+ if data_call .data .get (EVENT_ID )
458+ else None
459+ )
460+ self .interval = int (data_call .data .get (INTERVAL , 2 ))
461+ self .duration = int (data_call .data .get (DURATION , 10 ))
462+ self .max_frames = int (data_call .data .get (MAX_FRAMES , 3 ))
463+ self .target_width = data_call .data .get (TARGET_WIDTH , 3840 )
464+ self .temperature = float ()
465+ self .max_tokens = int (data_call .data .get (MAXTOKENS , 3000 ))
466+ self .include_filename = data_call .data .get (INCLUDE_FILENAME , False )
467+ self .expose_images = data_call .data .get (EXPOSE_IMAGES , False )
468+ self .generate_title = data_call .data .get (GENERATE_TITLE , False )
469+ self .sensor_entity = data_call .data .get (SENSOR_ENTITY , "" )
470+ self .response_format = data_call .data .get (RESPONSE_FORMAT , "text" )
471+ self .structure = data_call .data .get (STRUCTURE , None )
472+ self .title_field = data_call .data .get (TITLE_FIELD , "" )
473+ self .memory : Memory | None = None
474+
475+ # ------------ Create Event ------------
476+ self .title = data_call .data .get ("title" )
477+ self .description = data_call .data .get ("description" )
478+ self .start_time = data_call .data .get ("start_time" , dt_util .now ())
479+ self .start_time = self ._convert_time_input_to_datetime (self .start_time )
480+ self .end_time = data_call .data .get (
481+ "end_time" , self .start_time + timedelta (minutes = 1 )
482+ )
483+ self .end_time = self ._convert_time_input_to_datetime (self .end_time )
484+ self .image_path = data_call .data .get ("image_path" , "" )
485+ self .camera_entity = data_call .data .get ("camera_entity" , "" )
486+ self .label = data_call .data .get ("label" , "" )
487+
488+ # ------------ Added during call ------------
489+ # self.base64_images : List[str] = []
490+ # self.filenames : List[str] = []
491+
492+ def _convert_time_input_to_datetime (self , time_input ) -> datetime :
493+ """Convert time input to datetime object"""
494+
495+ if isinstance (time_input , datetime ):
496+ return time_input
497+ if isinstance (time_input , (int , float )):
498+ # Assume it's a Unix timestamp (seconds since epoch)
499+ return datetime .fromtimestamp (time_input )
500+ if isinstance (time_input , str ):
501+ # Try parsing ISO format first
502+ try :
503+ return datetime .fromisoformat (time_input )
504+ except ValueError :
505+ pass
506+ # Try parsing as timestamp string
507+ try :
508+ return datetime .fromtimestamp (float (time_input ))
509+ except Exception :
510+ pass
511+ raise ValueError (f"Unsupported date string format: { time_input } " )
512+ raise TypeError (f"Unsupported type for time_input: { type (time_input )} " )
513+
514+ def get (self , key , default = None ):
515+ return getattr (self , key , default )
516+
517+ def get_service_call_data (self ):
518+ return self
519+
520+
433521async def _create_event (
434522 hass ,
435- call : dict ,
523+ call : ServiceCallData ,
436524 start : datetime ,
437525 response : dict ,
438526 key_frame : str ,
439527) -> None :
440- if call .store_in_timeline :
528+ if call .get ( " store_in_timeline" ) :
441529 # Find timeline config
442530 config_entry = None
443531 for entry in hass .config_entries .async_entries (DOMAIN ):
@@ -453,17 +541,17 @@ async def _create_event(
453541
454542 timeline = Timeline (hass , config_entry )
455543
456- if call .image_entities and len (call .image_entities ) > 0 :
457- camera_name = call .image_entities [0 ]
458- elif call .video_paths and len (call .video_paths ) > 0 :
459- camera_name = call .video_paths [0 ].split ("/" )[- 1 ].replace (".mp4" , "" )
544+ image_entities = call .get ("image_entities" ) or []
545+ video_paths = call .get ("video_paths" ) or []
546+ if len (image_entities ) > 0 :
547+ camera_name = image_entities [0 ]
548+ elif len (video_paths ) > 0 :
549+ camera_name = video_paths [0 ].split ("/" )[- 1 ].replace (".mp4" , "" )
460550 else :
461551 camera_name = ""
462552
463- if "title" in response :
464- title = response .get ("title" )
465- else :
466- title = "Motion detected"
553+ title = response .get ("title" ) or "Motion detected"
554+ title = str (title )
467555
468556 await timeline .create_event (
469557 start = start ,
@@ -479,7 +567,7 @@ async def _create_event(
479567async def _update_sensor (hass , sensor_entity : str , value : str | int , type : str ) -> None :
480568 """Update the value of a sensor entity."""
481569 # Attempt to parse the response
482- value = value .strip ()
570+ value = str ( value ) .strip ()
483571 if type == "boolean" :
484572 if value .lower () in ["on" , "off" ]:
485573 new_value = value
@@ -533,90 +621,6 @@ async def _update_sensor(hass, sensor_entity: str, value: str | int, type: str)
533621 raise
534622
535623
536- class ServiceCallData :
537- """Store service call data and set default values"""
538-
539- def __init__ (self , data_call ):
540- # This is the config entry id
541- self .provider = str (data_call .data .get (PROVIDER ))
542- # If not set, the conf_default_model will be set in providers.py
543- self .model = data_call .data .get (MODEL )
544- self .message = str (data_call .data .get (MESSAGE , "" )[0 :2000 ])
545- self .store_in_timeline = data_call .data .get (STORE_IN_TIMELINE , False )
546- self .use_memory = data_call .data .get (USE_MEMORY , False )
547- self .image_paths = (
548- data_call .data .get (IMAGE_FILE , "" ).split ("\n " )
549- if data_call .data .get (IMAGE_FILE )
550- else None
551- )
552- self .image_entities = data_call .data .get (IMAGE_ENTITY )
553- self .video_paths = (
554- data_call .data .get (VIDEO_FILE , "" ).split ("\n " )
555- if data_call .data .get (VIDEO_FILE )
556- else None
557- )
558- self .event_id = (
559- data_call .data .get (EVENT_ID , "" ).split ("\n " )
560- if data_call .data .get (EVENT_ID )
561- else None
562- )
563- self .interval = int (data_call .data .get (INTERVAL , 2 ))
564- self .duration = int (data_call .data .get (DURATION , 10 ))
565- self .max_frames = int (data_call .data .get (MAX_FRAMES , 3 ))
566- self .target_width = data_call .data .get (TARGET_WIDTH , 3840 )
567- self .temperature = float ()
568- self .max_tokens = int (data_call .data .get (MAXTOKENS , 3000 ))
569- self .include_filename = data_call .data .get (INCLUDE_FILENAME , False )
570- self .expose_images = data_call .data .get (EXPOSE_IMAGES , False )
571- self .generate_title = data_call .data .get (GENERATE_TITLE , False )
572- self .sensor_entity = data_call .data .get (SENSOR_ENTITY , "" )
573- self .response_format = data_call .data .get (RESPONSE_FORMAT , "text" )
574- self .structure = data_call .data .get (STRUCTURE , None )
575- self .title_field = data_call .data .get (TITLE_FIELD , "" )
576-
577- # ------------ Create Event ------------
578- self .title = data_call .data .get ("title" )
579- self .description = data_call .data .get ("description" )
580- self .start_time = data_call .data .get ("start_time" , dt_util .now ())
581- self .start_time = self ._convert_time_input_to_datetime (self .start_time )
582- self .end_time = data_call .data .get (
583- "end_time" , self .start_time + timedelta (minutes = 1 )
584- )
585- self .end_time = self ._convert_time_input_to_datetime (self .end_time )
586- self .image_path = data_call .data .get ("image_path" , "" )
587- self .camera_entity = data_call .data .get ("camera_entity" , "" )
588- self .label = data_call .data .get ("label" , "" )
589-
590- # ------------ Added during call ------------
591- # self.base64_images : List[str] = []
592- # self.filenames : List[str] = []
593-
594- def _convert_time_input_to_datetime (self , time_input ) -> datetime :
595- """Convert time input to datetime object"""
596-
597- if isinstance (time_input , datetime ):
598- return time_input
599- if isinstance (time_input , (int , float )):
600- # Assume it's a Unix timestamp (seconds since epoch)
601- return datetime .fromtimestamp (time_input )
602- if isinstance (time_input , str ):
603- # Try parsing ISO format first
604- try :
605- return datetime .fromisoformat (time_input )
606- except ValueError :
607- pass
608- # Try parsing as timestamp string
609- try :
610- return datetime .fromtimestamp (float (time_input ))
611- except Exception :
612- pass
613- raise ValueError (f"Unsupported date string format: { time_input } " )
614- raise TypeError (f"Unsupported type for time_input: { type (time_input )} " )
615-
616- def get_service_call_data (self ):
617- return self
618-
619-
620624def setup (hass , config ):
621625 async def image_analyzer (data_call ):
622626 """Handle the service call to analyze an image with LLM Vision"""
@@ -660,7 +664,7 @@ async def image_analyzer(data_call):
660664
661665 await _create_event (
662666 hass = hass ,
663- call = call ,
667+ call = call , # type: ignore
664668 start = start ,
665669 response = response ,
666670 key_frame = processor .key_frame ,
@@ -698,7 +702,7 @@ async def video_analyzer(data_call):
698702
699703 await _create_event (
700704 hass = hass ,
701- call = call ,
705+ call = call , # type: ignore
702706 start = start ,
703707 response = response ,
704708 key_frame = processor .key_frame ,
@@ -739,7 +743,7 @@ async def stream_analyzer(data_call):
739743
740744 await _create_event (
741745 hass = hass ,
742- call = call ,
746+ call = call , # type: ignore
743747 start = start ,
744748 response = response ,
745749 key_frame = processor .key_frame ,
@@ -818,7 +822,7 @@ async def data_analyzer(data_call):
818822
819823 await _create_event (
820824 hass = hass ,
821- call = call ,
825+ call = call , # type: ignore
822826 start = start ,
823827 response = response ,
824828 key_frame = processor .key_frame ,
0 commit comments