Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion custom_components/frigate/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
SWITCH = "switch"
CAMERA = "camera"
IMAGE = "image"
SELECT = "select"
UPDATE = "update"
PLATFORMS = [SENSOR, CAMERA, IMAGE, NUMBER, SWITCH, BINARY_SENSOR, UPDATE]
PLATFORMS = [SENSOR, CAMERA, IMAGE, NUMBER, SELECT, SWITCH, BINARY_SENSOR, UPDATE]

# Device Classes
# This device class does not exist in HA, but we use it to be able
Expand Down
124 changes: 124 additions & 0 deletions custom_components/frigate/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Select platform for frigate."""

from __future__ import annotations

import logging
from typing import Any

from homeassistant.components.mqtt import async_publish
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import (
FrigateMQTTEntity,
ReceiveMessage,
decode_if_necessary,
get_frigate_device_identifier,
get_frigate_entity_unique_id,
verify_frigate_version,
)
from .const import ATTR_CONFIG, DOMAIN, NAME

_LOGGER: logging.Logger = logging.getLogger(__name__)


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Select entry setup."""
frigate_config = hass.data[DOMAIN][entry.entry_id][ATTR_CONFIG]

entities: list[SelectEntity] = []

# Profile selector requires Frigate 0.18+
if verify_frigate_version(frigate_config, "0.18"):
profiles = frigate_config.get("profiles", {})
if profiles:
entities.append(FrigateProfileSelect(entry, frigate_config, profiles))

async_add_entities(entities)


class FrigateProfileSelect(FrigateMQTTEntity, SelectEntity):
"""Frigate Profile Select class."""

_attr_entity_category = EntityCategory.CONFIG

def __init__(
self,
config_entry: ConfigEntry,
frigate_config: dict[str, Any],
profiles: dict[str, Any],
) -> None:
"""Construct a FrigateProfileSelect."""
self._frigate_config = frigate_config
self._profiles = profiles
self._attr_options = list(profiles.keys())
self._attr_current_option = None
self._command_topic = f"{frigate_config['mqtt']['topic_prefix']}/profile/set"

super().__init__(
config_entry,
frigate_config,
{
"state_topic": {
"msg_callback": self._state_message_received,
"qos": 0,
"topic": (
f"{frigate_config['mqtt']['topic_prefix']}/profile/state"
),
},
},
)

@callback
def _state_message_received(self, msg: ReceiveMessage) -> None:
"""Handle a new received MQTT state message."""
payload = decode_if_necessary(msg.payload)
if payload in self._attr_options:
self._attr_current_option = payload
self.async_write_ha_state()

@property
def unique_id(self) -> str:
"""Return a unique ID to use for this entity."""
return get_frigate_entity_unique_id(
self._config_entry.entry_id,
"select",
"profile",
)

@property
def device_info(self) -> DeviceInfo:
"""Get device information."""
return {
"identifiers": {get_frigate_device_identifier(self._config_entry)},
"name": NAME,
"model": self._get_model(),
"configuration_url": self._config_entry.data.get(CONF_URL),
"manufacturer": NAME,
}

@property
def name(self) -> str:
"""Return the name of the entity."""
return "Profile"

@property
def icon(self) -> str:
"""Return the icon of the entity."""
return "mdi:home-account"

async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await async_publish(
self.hass,
self._command_topic,
option,
0,
False,
)
14 changes: 12 additions & 2 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,16 @@
TEST_SENSOR_GLOBAL_CLASSIFICATION_RED_SHIRT = "sensor.frigate_red_shirt_last_camera"
TEST_UPDATE_FRIGATE_CONTAINER_ENTITY_ID = "update.frigate_server"

TEST_SERVER_VERSION = "0.17.0"
TEST_SELECT_PROFILE_ENTITY_ID = "select.frigate_profile"

TEST_SERVER_VERSION = "0.18.0"
TEST_CONFIG_ENTRY_ID = "74565ad414754616000674c87bdc876c"
TEST_URL = "http://example.com"
TEST_USERNAME = "secret_username"
TEST_PASSWORD = "secret_password"
TEST_FRIGATE_INSTANCE_ID = "frigate_client_id"
TEST_CONFIG = {
"version": "0.17-0",
"version": "0.18-0",
"cameras": {
"front_door": {
"best_image_timeout": 60,
Expand Down Expand Up @@ -224,6 +226,14 @@
"topic_prefix": "frigate",
"user": None,
},
"profiles": {
"away": {
"friendly_name": "Away",
},
"home": {
"friendly_name": "Home",
},
},
"snapshots": {"retain": {"default": 10, "objects": {}}},
"go2rtc": {"streams": {"front_door": "rtsp://rtsp:password@cam-front-door/live"}},
"classification": {
Expand Down
30 changes: 5 additions & 25 deletions tests/test_llm_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import copy
import logging
from unittest.mock import AsyncMock

Expand All @@ -16,7 +15,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers import llm

from . import TEST_CONFIG, create_mock_frigate_client, setup_mock_frigate_config_entry
from . import create_mock_frigate_client, setup_mock_frigate_config_entry

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -154,18 +153,9 @@ async def test_frigate_query_tool_api_error(hass: HomeAssistant) -> None:
assert "error" in result


def _create_018_client() -> AsyncMock:
"""Create a mock client with Frigate 0.18 config."""
client = create_mock_frigate_client()
config = copy.deepcopy(TEST_CONFIG)
config["version"] = "0.18.0"
client.async_get_config = AsyncMock(return_value=config)
return client


async def test_frigate_query_tool_skips_non_entry_data(hass: HomeAssistant) -> None:
"""Test FrigateQueryTool skips non-dict entries like llm_unregister callback."""
client = _create_018_client()
client = create_mock_frigate_client()
client.async_chat_completion = AsyncMock(
return_value={
"message": {"role": "assistant", "content": "All clear."},
Expand All @@ -190,8 +180,7 @@ async def test_frigate_query_tool_skips_non_entry_data(hass: HomeAssistant) -> N

async def test_frigate_service_api_skips_non_entry_data(hass: HomeAssistant) -> None:
"""Test FrigateServiceAPI skips non-dict entries when collecting cameras."""
client = _create_018_client()
await setup_mock_frigate_config_entry(hass, client=client)
await setup_mock_frigate_config_entry(hass)

# ATTR_LLM_UNREGISTER is now in hass.data[DOMAIN] as a callable (non-dict)
assert ATTR_LLM_UNREGISTER in hass.data[DOMAIN]
Expand All @@ -203,28 +192,19 @@ async def test_frigate_service_api_skips_non_entry_data(hass: HomeAssistant) ->

async def test_llm_api_registration_with_version_018(hass: HomeAssistant) -> None:
"""Test LLM API is registered when Frigate version is 0.18+."""
client = _create_018_client()
await setup_mock_frigate_config_entry(hass, client=client)
await setup_mock_frigate_config_entry(hass)

assert ATTR_LLM_UNREGISTER in hass.data[DOMAIN]
apis = llm.async_get_apis(hass)
api_ids = [api.id for api in apis]
assert FRIGATE_SERVICES_API_ID in api_ids


async def test_llm_api_not_registered_with_version_017(hass: HomeAssistant) -> None:
"""Test LLM API is not registered when Frigate version is below 0.18."""
await setup_mock_frigate_config_entry(hass)

assert ATTR_LLM_UNREGISTER not in hass.data[DOMAIN]


async def test_llm_api_unregistered_on_last_entry_unload(
hass: HomeAssistant,
) -> None:
"""Test LLM API is unregistered when last config entry is unloaded."""
client = _create_018_client()
config_entry = await setup_mock_frigate_config_entry(hass, client=client)
config_entry = await setup_mock_frigate_config_entry(hass)
assert ATTR_LLM_UNREGISTER in hass.data[DOMAIN]

await hass.config_entries.async_unload(config_entry.entry_id)
Expand Down
Loading
Loading