Skip to content

Commit

Permalink
Merge branch 'image-calendar' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
valentinfrlch authored Feb 4, 2025
2 parents eccc698 + 76c42df commit aafe77b
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 139 deletions.
70 changes: 38 additions & 32 deletions custom_components/llmvision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ async def async_setup_entry(hass, entry):
ollama_https = entry.data.get(CONF_OLLAMA_HTTPS)
custom_openai_endpoint = entry.data.get(CONF_CUSTOM_OPENAI_ENDPOINT)
custom_openai_api_key = entry.data.get(CONF_CUSTOM_OPENAI_API_KEY)
custom_openai_default_model = entry.data.get(CONF_CUSTOM_OPENAI_DEFAULT_MODEL)
custom_openai_default_model = entry.data.get(
CONF_CUSTOM_OPENAI_DEFAULT_MODEL)
retention_time = entry.data.get(CONF_RETENTION_TIME)
aws_access_key_id = entry.data.get(CONF_AWS_ACCESS_KEY_ID)
aws_secret_access_key = entry.data.get(CONF_AWS_SECRET_ACCESS_KEY)
Expand Down Expand Up @@ -170,7 +171,7 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry) -> bool:
return False


async def _remember(hass, call, start, response) -> None:
async def _remember(hass, call, start, response, key_frame) -> None:
if call.remember:
# Find semantic index config
config_entry = None
Expand All @@ -186,37 +187,27 @@ async def _remember(hass, call, start, response) -> None:

semantic_index = SemanticIndex(hass, config_entry)

if "title" in response:
title = response.get("title", "Unknown object seen")
if call.image_entities and len(call.image_entities) > 0:
camera_name = call.image_entities[0]
elif call.video_paths and len(call.video_paths) > 0:
camera_name = call.video_paths[0].split(
"/")[-1].replace(".mp4", "")
else:
camera_name = "File Input"

if "title" not in response:
if call.image_entities and len(call.image_entities) > 0:
camera_name = call.image_entities[0]
title = "Motion detected near " + camera_name
elif call.video_paths and len(call.video_paths) > 0:
camera_name = call.video_paths[0].split(
"/")[-1].replace(".mp4", "")
title = "Motion detected in " + camera_name
else:
camera_name = "File Input"
title = "Motion detected"
if call.image_entities and len(call.image_entities) > 0:
camera_name = call.image_entities[0]
title = "Motion detected near " + camera_name
elif call.video_paths and len(call.video_paths) > 0:
camera_name = call.video_paths[0].split(
"/")[-1].replace(".mp4", "")
title = "Motion detected in " + camera_name
else:
camera_name = ""
title = "Motion detected"

if "response_text" not in response:
raise ValueError("response_text is missing in the response")
if "title" in response:
title = response.get("title")

await semantic_index.remember(
start=start,
end=dt_util.now() + timedelta(minutes=1),
label=title,
camera_name=camera_name,
summary=response["response_text"]
summary=response["response_text"],
key_frame=key_frame,
camera_name=camera_name
)


Expand Down Expand Up @@ -337,12 +328,17 @@ async def image_analyzer(data_call):
image_paths=call.image_paths,
target_width=call.target_width,
include_filename=call.include_filename,
expose_images=call.expose_images
expose_images=call.expose_images,
expose_images_persist=call.expose_images_persist
)

# Validate configuration, input data and make the call
response = await request.call(call)
await _remember(hass, call, start, response)
await _remember(hass=hass,
call=call,
start=start,
response=response,
key_frame=processor.key_frame)
return response

async def video_analyzer(data_call):
Expand All @@ -368,7 +364,11 @@ async def video_analyzer(data_call):
frigate_retry_seconds=call.frigate_retry_seconds
)
response = await request.call(call)
await _remember(hass, call, start, response)
await _remember(hass=hass,
call=call,
start=start,
response=response,
key_frame=processor.key_frame)
return response

async def stream_analyzer(data_call):
Expand All @@ -382,16 +382,22 @@ async def stream_analyzer(data_call):
temperature=call.temperature,
)
processor = MediaProcessor(hass, request)

request = await processor.add_streams(image_entities=call.image_entities,
duration=call.duration,
max_frames=call.max_frames,
target_width=call.target_width,
include_filename=call.include_filename,
expose_images=call.expose_images
expose_images=call.expose_images,
expose_images_persist=call.expose_images_persist
)

response = await request.call(call)
await _remember(hass, call, start, response)
await _remember(hass=hass,
call=call,
start=start,
response=response,
key_frame=processor.key_frame)
return response

async def data_analyzer(data_call):
Expand Down
38 changes: 22 additions & 16 deletions custom_components/llmvision/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class SemanticIndex(CalendarEntity):
"""Representation of a Calendar."""

def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
"""Initialize the calendar."""
"""Initialize the calendar"""
self.hass = hass
self._attr_name = config_entry.title
self._attr_unique_id = config_entry.entry_id
Expand All @@ -35,16 +35,18 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
self._attr_unique_id).get(CONF_RETENTION_TIME)
self._current_event = None
self._attr_supported_features = (CalendarEntityFeature.DELETE_EVENT)

# Path to the JSON file where events are stored
self._file_path = os.path.join(
self.hass.config.path("llmvision"), "events.json"
)

# Ensure the directory exists
os.makedirs(os.path.dirname(self._file_path), exist_ok=True)
self.hass.loop.create_task(self.async_update())

def _ensure_datetime(self, dt):
"""Ensure the input is a datetime.datetime object."""
"""Ensure the input is a datetime.datetime object"""
if isinstance(dt, datetime.date) and not isinstance(dt, datetime.datetime):
dt = datetime.datetime.combine(dt, datetime.datetime.min.time())
if dt.tzinfo is None:
Expand All @@ -57,7 +59,7 @@ async def async_get_events(
start_date: datetime.datetime,
end_date: datetime.datetime,
) -> list[CalendarEvent]:
"""Return calendar events within a datetime range."""
"""Return calendar events within a datetime range"""
events = []

# Ensure start_date and end_date are datetime.datetime objects and timezone-aware
Expand All @@ -75,18 +77,23 @@ async def async_get_events(

@property
def extra_state_attributes(self):
"""Return the state attributes."""
"""Return the state attributes"""
return {
"events": [event.summary for event in self._events],
"starts": [event.start for event in self._events],
"ends": [event.end for event in self._events],
"summaries": [event.summary for event in self._events],
"key_frames": [event.location.split(",")[0] for event in self._events],
"camera_names": [event.location.split(",")[1] if len(event.location.split(",")) > 1 else "" for event in self._events],
}

@property
def event(self):
"""Return the current event."""
"""Return the current event"""
return self._current_event

async def async_create_event(self, **kwargs: any) -> None:
"""Add a new event to calendar."""
"""Add a new event to calendar"""
await self.async_update()
dtstart = kwargs[EVENT_START]
dtend = kwargs[EVENT_END]
Expand Down Expand Up @@ -127,7 +134,7 @@ async def async_delete_event(
await self._save_events()

async def async_update(self) -> None:
"""Load events from the JSON file."""
"""Load events from JSON"""
def read_from_file():
if os.path.exists(self._file_path):
with open(self._file_path, 'r') as file:
Expand All @@ -142,18 +149,17 @@ def read_from_file():
start=dt_util.as_local(dt_util.parse_datetime(event["start"])),
end=dt_util.as_local(dt_util.parse_datetime(event["end"])),
description=event.get("description"),
location=event.get("location"),
location=event.get("location")
)
for event in events_data
]
# _LOGGER.info(f"events: {self._events}")

async def _save_events(self) -> None:
"""Save events to the JSON file."""
"""Save events to JSON"""
# Delete events outside of retention time window
now = datetime.datetime.now()
cutoff_date = now - datetime.timedelta(days=self._retention_time)

if self._retention_time != 0:
_LOGGER.info(f"Deleting events before {cutoff_date}")

Expand All @@ -164,7 +170,7 @@ async def _save_events(self) -> None:
"start": dt_util.as_local(self._ensure_datetime(event.start)).isoformat(),
"end": dt_util.as_local(self._ensure_datetime(event.end)).isoformat(),
"description": event.description,
"location": event.location,
"location": event.location
}
for event in self._events
if dt_util.as_local(self._ensure_datetime(event.end)) >= self._ensure_datetime(cutoff_date) or self._retention_time == 0
Expand All @@ -176,13 +182,13 @@ def write_to_file():

await self.hass.loop.run_in_executor(None, write_to_file)

async def remember(self, start, end, label, camera_name, summary):
"""Remember the event."""
async def remember(self, start, end, label, key_frame, summary, camera_name=""):
"""Remember the event"""
await self.async_create_event(
dtstart=start,
dtend=end,
summary=label,
location=camera_name,
location=key_frame + "," + camera_name,
description=summary,
)

Expand Down
Loading

0 comments on commit aafe77b

Please sign in to comment.