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
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async def generate_content(call: ServiceCall) -> ServiceResponse:

prompt_parts.extend(
await async_prepare_files_for_prompt(
hass, client, [Path(filename) for filename in files]
hass, client, [(Path(filename), None) for filename in files]
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async def _async_generate_image(
await async_prepare_files_for_prompt(
self.hass,
self._genai_client,
[a.path for a in user_message.attachments],
[(a.path, a.mime_type) for a in user_message.attachments],
)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ async def _async_handle_chat_log(
await async_prepare_files_for_prompt(
self.hass,
self._genai_client,
[a.path for a in user_message.attachments],
[(a.path, a.mime_type) for a in user_message.attachments],
)
)

Expand Down Expand Up @@ -526,7 +526,7 @@ def create_generate_content_config(self) -> GenerateContentConfig:


async def async_prepare_files_for_prompt(
hass: HomeAssistant, client: Client, files: list[Path]
hass: HomeAssistant, client: Client, files: list[tuple[Path, str | None]]
) -> list[File]:
"""Upload files so they can be attached to a prompt.

Expand All @@ -535,10 +535,11 @@ async def async_prepare_files_for_prompt(

def upload_files() -> list[File]:
prompt_parts: list[File] = []
for filename in files:
for filename, mimetype in files:
if not filename.exists():
raise HomeAssistantError(f"`{filename}` does not exist")
mimetype = mimetypes.guess_type(filename)[0]
if mimetype is None:
mimetype = mimetypes.guess_type(filename)[0]
prompt_parts.append(
client.files.upload(
file=filename,
Expand Down
94 changes: 65 additions & 29 deletions homeassistant/components/history_stats/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
StateSelector,
StateSelectorConfig,
TemplateSelector,
TextSelector,
TextSelectorConfig,
)
from homeassistant.helpers.template import Template

Expand Down Expand Up @@ -67,7 +68,6 @@ async def validate_options(
{
vol.Required(CONF_NAME, default=DEFAULT_NAME): TextSelector(),
vol.Required(CONF_ENTITY_ID): EntitySelector(),
vol.Required(CONF_STATE): TextSelector(TextSelectorConfig(multiple=True)),
vol.Required(CONF_TYPE, default=CONF_TYPE_TIME): SelectSelector(
SelectSelectorConfig(
options=CONF_TYPE_KEYS,
Expand All @@ -77,44 +77,78 @@ async def validate_options(
),
}
)
DATA_SCHEMA_OPTIONS = vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): EntitySelector(
EntitySelectorConfig(read_only=True)
),
vol.Optional(CONF_STATE): TextSelector(
TextSelectorConfig(multiple=True, read_only=True)
),
vol.Optional(CONF_TYPE): SelectSelector(
SelectSelectorConfig(
options=CONF_TYPE_KEYS,
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_TYPE,
read_only=True,
)
),
vol.Optional(CONF_START): TemplateSelector(),
vol.Optional(CONF_END): TemplateSelector(),
vol.Optional(CONF_DURATION): DurationSelector(
DurationSelectorConfig(enable_day=True, allow_negative=False)
),
}
)


async def get_state_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Return schema for state step."""
entity_id = handler.options[CONF_ENTITY_ID]

return vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): EntitySelector(
EntitySelectorConfig(read_only=True)
),
vol.Required(CONF_STATE): StateSelector(
StateSelectorConfig(
multiple=True,
entity_id=entity_id,
)
),
}
)


async def get_options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
"""Return schema for options step."""
entity_id = handler.options[CONF_ENTITY_ID]
return _get_options_schema_with_entity_id(entity_id)


def _get_options_schema_with_entity_id(entity_id: str) -> vol.Schema:
return vol.Schema(
{
vol.Optional(CONF_ENTITY_ID): EntitySelector(
EntitySelectorConfig(read_only=True)
),
vol.Optional(CONF_STATE): StateSelector(
StateSelectorConfig(
multiple=True,
entity_id=entity_id,
read_only=True,
)
),
vol.Optional(CONF_TYPE): SelectSelector(
SelectSelectorConfig(
options=CONF_TYPE_KEYS,
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_TYPE,
read_only=True,
)
),
vol.Optional(CONF_START): TemplateSelector(),
vol.Optional(CONF_END): TemplateSelector(),
vol.Optional(CONF_DURATION): DurationSelector(
DurationSelectorConfig(enable_day=True, allow_negative=False)
),
}
)


CONFIG_FLOW = {
"user": SchemaFlowFormStep(
schema=DATA_SCHEMA_SETUP,
next_step="options",
next_step="state",
),
"state": SchemaFlowFormStep(schema=get_state_schema, next_step="options"),
"options": SchemaFlowFormStep(
schema=DATA_SCHEMA_OPTIONS,
schema=get_options_schema,
validate_user_input=validate_options,
preview="history_stats",
),
}
OPTIONS_FLOW = {
"init": SchemaFlowFormStep(
DATA_SCHEMA_OPTIONS,
schema=get_options_schema,
validate_user_input=validate_options,
preview="history_stats",
),
Expand Down Expand Up @@ -198,7 +232,9 @@ def async_preview_updated(

validated_data: Any = None
try:
validated_data = DATA_SCHEMA_OPTIONS(msg["user_input"])
validated_data = (_get_options_schema_with_entity_id(entity_id))(
msg["user_input"]
)
except vol.Invalid as ex:
connection.send_error(msg["id"], "invalid_schema", str(ex))
return
Expand Down
10 changes: 10 additions & 0 deletions homeassistant/components/history_stats/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
"type": "The type of sensor, one of 'time', 'ratio' or 'count'"
}
},
"state": {
"data": {
"entity_id": "[%key:component::history_stats::config::step::user::data::entity_id%]",
"state": "[%key:component::history_stats::config::step::user::data::state%]"
},
"data_description": {
"entity_id": "[%key:component::history_stats::config::step::user::data_description::entity_id%]",
"state": "[%key:component::history_stats::config::step::user::data_description::state%]"
}
},
"options": {
"description": "Read the documentation for further details on how to configure the history stats sensor using these options.",
"data": {
Expand Down
54 changes: 47 additions & 7 deletions homeassistant/components/homekit_controller/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
EncryptionError,
)
from aiohomekit.model import Accessories, Accessory, Transport
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.characteristics import (
EVENT_CHARACTERISTICS,
Characteristic,
CharacteristicPermissions,
CharacteristicsTypes,
)
from aiohomekit.model.services import Service, ServicesTypes

from homeassistant.components.thread import async_get_preferred_dataset
Expand Down Expand Up @@ -179,6 +184,21 @@ def remove_pollable_characteristics(
for aid_iid in characteristics:
self.pollable_characteristics.discard(aid_iid)

def get_all_pollable_characteristics(self) -> set[tuple[int, int]]:
"""Get all characteristics that can be polled.

This is used during startup to poll all readable characteristics
before entities have registered what they care about.
"""
return {
(accessory.aid, char.iid)
for accessory in self.entity_map.accessories
for service in accessory.services
for char in service.characteristics
if CharacteristicPermissions.paired_read in char.perms
and char.type not in EVENT_CHARACTERISTICS
}

def add_watchable_characteristics(
self, characteristics: list[tuple[int, int]]
) -> None:
Expand Down Expand Up @@ -309,9 +329,13 @@ async def async_setup(self) -> None:
await self.async_process_entity_map()

if transport != Transport.BLE:
# Do a single poll to make sure the chars are
# up to date so we don't restore old data.
await self.async_update()
# When Home Assistant starts, we restore the accessory map from storage
# which contains characteristic values from when HA was last running.
# These values are stale and may be incorrect (e.g., Ecobee thermostats
# report 100°C when restarting). We need to poll for fresh values before
# creating entities. Use poll_all=True since entities haven't registered
# their characteristics yet.
await self.async_update(poll_all=True)
self._async_start_polling()

# If everything is up to date, we can create the entities
Expand Down Expand Up @@ -863,9 +887,25 @@ async def async_request_update(self, now: datetime | None = None) -> None:
"""Request an debounced update from the accessory."""
await self._debounced_update.async_call()

async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
async def async_update(
self, now: datetime | None = None, *, poll_all: bool = False
) -> None:
"""Poll state of all entities attached to this bridge/accessory.

Args:
now: The current time (used by time interval callbacks).
poll_all: If True, poll all readable characteristics instead
of just the registered ones.
This is useful during initial setup before entities have
registered their characteristics.
"""
if poll_all:
# Poll all readable characteristics during initial startup
# excluding device trigger characteristics (buttons, doorbell, etc.)
to_poll = self.get_all_pollable_characteristics()
else:
to_poll = self.pollable_characteristics

if not to_poll:
self.async_update_available_state()
_LOGGER.debug(
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/image_upload/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from homeassistant.helpers.typing import ConfigType, VolDictType
from homeassistant.util import dt as dt_util

from .const import DOMAIN
from .const import DOMAIN, FOLDER_IMAGE

_LOGGER = logging.getLogger(__name__)
STORAGE_KEY = "image"
Expand All @@ -45,7 +45,7 @@

async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Image integration."""
image_dir = pathlib.Path(hass.config.path("image"))
image_dir = pathlib.Path(hass.config.path(FOLDER_IMAGE))
hass.data[DOMAIN] = storage_collection = ImageStorageCollection(hass, image_dir)
await storage_collection.async_load()
ImageUploadStorageCollectionWebsocket(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/image_upload/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Constants for the Image Upload integration."""

DOMAIN = "image_upload"
FOLDER_IMAGE = "image"
15 changes: 13 additions & 2 deletions homeassistant/components/image_upload/media_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from __future__ import annotations

import pathlib

from propcache.api import cached_property

from homeassistant.components.media_player import BrowseError, MediaClass
from homeassistant.components.media_source import (
BrowseMediaSource,
Expand All @@ -12,7 +16,7 @@
)
from homeassistant.core import HomeAssistant

from .const import DOMAIN
from .const import DOMAIN, FOLDER_IMAGE


async def async_get_media_source(hass: HomeAssistant) -> ImageUploadMediaSource:
Expand All @@ -30,6 +34,11 @@ def __init__(self, hass: HomeAssistant) -> None:
super().__init__(DOMAIN)
self.hass = hass

@cached_property
def image_folder(self) -> pathlib.Path:
"""Return the image folder path."""
return pathlib.Path(self.hass.config.path(FOLDER_IMAGE))

async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
image = self.hass.data[DOMAIN].data.get(item.identifier)
Expand All @@ -38,7 +47,9 @@ async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
raise Unresolvable(f"Could not resolve media item: {item.identifier}")

return PlayMedia(
f"/api/image/serve/{image['id']}/original", image["content_type"]
f"/api/image/serve/{image['id']}/original",
image["content_type"],
path=self.image_folder / item.identifier / "original",
)

async def async_browse_media(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/sleep_as_android/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/sleep_as_android",
"iot_class": "local_push",
"quality_scale": "silver"
"quality_scale": "platinum"
}
4 changes: 1 addition & 3 deletions tests/components/bluetooth/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from datetime import timedelta
import time
from typing import Any
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch

from bleak import BleakError
from bleak.backends.scanner import AdvertisementData, BLEDevice
Expand Down Expand Up @@ -140,7 +140,6 @@ def register_detection_callback(self, *args, **kwargs):
"adapter": "hci0",
"bluez": scanner.PASSIVE_SCANNER_ARGS, # pylint: disable=c-extension-no-member
"scanning_mode": "passive",
"detection_callback": ANY,
}


Expand Down Expand Up @@ -190,7 +189,6 @@ def register_detection_callback(self, *args, **kwargs):
assert init_kwargs == {
"adapter": "hci0",
"scanning_mode": "active",
"detection_callback": ANY,
}


Expand Down
Loading
Loading