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
2 changes: 0 additions & 2 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion homeassistant/components/aosmith/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
AOSmithStatusCoordinator,
)

PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.WATER_HEATER]
PLATFORMS: list[Platform] = [Platform.SELECT, Platform.SENSOR, Platform.WATER_HEATER]


async def async_setup_entry(hass: HomeAssistant, entry: AOSmithConfigEntry) -> bool:
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/aosmith/icons.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
{
"entity": {
"select": {
"hot_water_plus_level": {
"default": "mdi:water-plus"
}
},
"sensor": {
"hot_water_availability": {
"default": "mdi:water-thermometer"
Expand Down
70 changes: 70 additions & 0 deletions homeassistant/components/aosmith/select.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""The select platform for the A. O. Smith integration."""

from homeassistant.components.select import SelectEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from . import AOSmithConfigEntry
from .coordinator import AOSmithStatusCoordinator
from .entity import AOSmithStatusEntity

HWP_LEVEL_HA_TO_AOSMITH = {
"off": 0,
"level1": 1,
"level2": 2,
"level3": 3,
}
HWP_LEVEL_AOSMITH_TO_HA = {value: key for key, value in HWP_LEVEL_HA_TO_AOSMITH.items()}


async def async_setup_entry(
hass: HomeAssistant,
entry: AOSmithConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up A. O. Smith select platform."""
data = entry.runtime_data

async_add_entities(
AOSmithHotWaterPlusSelectEntity(data.status_coordinator, device.junction_id)
for device in data.status_coordinator.data.values()
if device.supports_hot_water_plus
)


class AOSmithHotWaterPlusSelectEntity(AOSmithStatusEntity, SelectEntity):
"""Class for the Hot Water+ select entity."""

_attr_translation_key = "hot_water_plus_level"
_attr_options = list(HWP_LEVEL_HA_TO_AOSMITH)

def __init__(self, coordinator: AOSmithStatusCoordinator, junction_id: str) -> None:
"""Initialize the entity."""
super().__init__(coordinator, junction_id)
self._attr_unique_id = f"hot_water_plus_level_{junction_id}"

@property
def suggested_object_id(self) -> str | None:
"""Override the suggested object id to make '+' get converted to 'plus' in the entity id."""
return "hot_water_plus_level"

@property
def current_option(self) -> str | None:
"""Return the current Hot Water+ mode."""
hot_water_plus_level = self.device.status.hot_water_plus_level
return (
None
if hot_water_plus_level is None
else HWP_LEVEL_AOSMITH_TO_HA.get(hot_water_plus_level)
)

async def async_select_option(self, option: str) -> None:
"""Set the Hot Water+ mode."""
aosmith_hwp_level = HWP_LEVEL_HA_TO_AOSMITH[option]
await self.client.update_mode(
junction_id=self.junction_id,
mode=self.device.status.current_mode,
hot_water_plus_level=aosmith_hwp_level,
)

await self.coordinator.async_request_refresh()
11 changes: 11 additions & 0 deletions homeassistant/components/aosmith/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@
}
},
"entity": {
"select": {
"hot_water_plus_level": {
"name": "Hot Water+ level",
"state": {
"off": "[%key:common::state::off%]",
"level1": "Level 1",
"level2": "Level 2",
"level3": "Level 3"
}
}
},
"sensor": {
"hot_water_availability": {
"name": "Hot water availability"
Expand Down
26 changes: 24 additions & 2 deletions homeassistant/components/assist_pipeline/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from collections.abc import Iterable
from dataclasses import replace

from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory, Platform
Expand Down Expand Up @@ -64,15 +65,36 @@ class AssistPipelineSelect(SelectEntity, restore_state.RestoreEntity):
translation_key="pipeline",
entity_category=EntityCategory.CONFIG,
)

_attr_should_poll = False
_attr_current_option = OPTION_PREFERRED
_attr_options = [OPTION_PREFERRED]

def __init__(self, hass: HomeAssistant, domain: str, unique_id_prefix: str) -> None:
def __init__(
self,
hass: HomeAssistant,
domain: str,
unique_id_prefix: str,
index: int = 0,
) -> None:
"""Initialize a pipeline selector."""
if index < 1:
# Keep compatibility
key_suffix = ""
placeholder = ""
else:
key_suffix = f"_{index + 1}"
placeholder = f" {index + 1}"

self.entity_description = replace(
self.entity_description,
key=f"pipeline{key_suffix}",
translation_placeholders={"index": placeholder},
)

self._domain = domain
self._unique_id_prefix = unique_id_prefix
self._attr_unique_id = f"{unique_id_prefix}-pipeline"
self._attr_unique_id = f"{unique_id_prefix}-{self.entity_description.key}"
self.hass = hass
self._update_options()

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/assist_pipeline/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"select": {
"pipeline": {
"name": "Assistant",
"name": "Assistant{index}",
"state": {
"preferred": "Preferred"
}
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/enocean/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"domain": "enocean",
"name": "EnOcean",
"codeowners": ["@bdurrer"],
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/enocean",
"iot_class": "local_push",
Expand Down
77 changes: 57 additions & 20 deletions homeassistant/components/esphome/assist_satellite.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,27 +127,39 @@ def __init__(self, entry: ESPHomeConfigEntry) -> None:
available_wake_words=[], active_wake_words=[], max_active_wake_words=1
)

@property
def pipeline_entity_id(self) -> str | None:
"""Return the entity ID of the pipeline to use for the next conversation."""
assert self._entry_data.device_info is not None
self._active_pipeline_index = 0

def _get_entity_id(self, suffix: str) -> str | None:
"""Return the entity id for pipeline select, etc."""
if self._entry_data.device_info is None:
return None

ent_reg = er.async_get(self.hass)
return ent_reg.async_get_entity_id(
Platform.SELECT,
DOMAIN,
f"{self._entry_data.device_info.mac_address}-pipeline",
f"{self._entry_data.device_info.mac_address}-{suffix}",
)

@property
def pipeline_entity_id(self) -> str | None:
"""Return the entity ID of the primary pipeline to use for the next conversation."""
return self.get_pipeline_entity(self._active_pipeline_index)

def get_pipeline_entity(self, index: int) -> str | None:
"""Return the entity ID of a pipeline by index."""
id_suffix = "" if index < 1 else f"_{index + 1}"
return self._get_entity_id(f"pipeline{id_suffix}")

def get_wake_word_entity(self, index: int) -> str | None:
"""Return the entity ID of a wake word by index."""
id_suffix = "" if index < 1 else f"_{index + 1}"
return self._get_entity_id(f"wake_word{id_suffix}")

@property
def vad_sensitivity_entity_id(self) -> str | None:
"""Return the entity ID of the VAD sensitivity to use for the next conversation."""
assert self._entry_data.device_info is not None
ent_reg = er.async_get(self.hass)
return ent_reg.async_get_entity_id(
Platform.SELECT,
DOMAIN,
f"{self._entry_data.device_info.mac_address}-vad_sensitivity",
)
return self._get_entity_id("vad_sensitivity")

@callback
def async_get_configuration(
Expand Down Expand Up @@ -235,6 +247,7 @@ async def async_added_to_hass(self) -> None:
)
)

assert self._attr_supported_features is not None
if feature_flags & VoiceAssistantFeature.ANNOUNCE:
# Device supports announcements
self._attr_supported_features |= (
Expand All @@ -257,8 +270,8 @@ async def async_added_to_hass(self) -> None:

# Update wake word select when config is updated
self.async_on_remove(
self._entry_data.async_register_assist_satellite_set_wake_word_callback(
self.async_set_wake_word
self._entry_data.async_register_assist_satellite_set_wake_words_callback(
self.async_set_wake_words
)
)

Expand Down Expand Up @@ -482,8 +495,31 @@ async def handle_pipeline_start(
# ANNOUNCEMENT format from media player
self._update_tts_format()

# Run the pipeline
_LOGGER.debug("Running pipeline from %s to %s", start_stage, end_stage)
# Run the appropriate pipeline.
self._active_pipeline_index = 0

maybe_pipeline_index = 0
while True:
if not (ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index)):
break

if not (ww_state := self.hass.states.get(ww_entity_id)):
continue

if ww_state.state == wake_word_phrase:
# First match
self._active_pipeline_index = maybe_pipeline_index
break

# Try next wake word select
maybe_pipeline_index += 1

_LOGGER.debug(
"Running pipeline %s from %s to %s",
self._active_pipeline_index + 1,
start_stage,
end_stage,
)
self._pipeline_task = self.config_entry.async_create_background_task(
self.hass,
self.async_accept_pipeline_from_satellite(
Expand Down Expand Up @@ -514,6 +550,7 @@ async def handle_pipeline_stop(self, abort: bool) -> None:
def handle_pipeline_finished(self) -> None:
"""Handle when pipeline has finished running."""
self._stop_udp_server()
self._active_pipeline_index = 0
_LOGGER.debug("Pipeline finished")

def handle_timer_event(
Expand Down Expand Up @@ -542,15 +579,15 @@ async def handle_announcement_finished(
self.tts_response_finished()

@callback
def async_set_wake_word(self, wake_word_id: str) -> None:
"""Set active wake word and update config on satellite."""
self._satellite_config.active_wake_words = [wake_word_id]
def async_set_wake_words(self, wake_word_ids: list[str]) -> None:
"""Set active wake words and update config on satellite."""
self._satellite_config.active_wake_words = wake_word_ids
self.config_entry.async_create_background_task(
self.hass,
self.async_set_configuration(self._satellite_config),
"esphome_voice_assistant_set_config",
)
_LOGGER.debug("Setting active wake word: %s", wake_word_id)
_LOGGER.debug("Setting active wake word(s): %s", wake_word_ids)

def _update_tts_format(self) -> None:
"""Update the TTS format from the first media player."""
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/esphome/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@
# ESPHome always uses .0 for the changelog URL
STABLE_BLE_URL_VERSION = f"{STABLE_BLE_VERSION.major}.{STABLE_BLE_VERSION.minor}.0"
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_URL_VERSION}.html"

NO_WAKE_WORD: Final[str] = "no_wake_word"
30 changes: 20 additions & 10 deletions homeassistant/components/esphome/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,10 @@ class RuntimeEntryData:
assist_satellite_config_update_callbacks: list[
Callable[[AssistSatelliteConfiguration], None]
] = field(default_factory=list)
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
default_factory=list
assist_satellite_set_wake_words_callbacks: list[Callable[[list[str]], None]] = (
field(default_factory=list)
)
assist_satellite_wake_words: dict[int, str] = field(default_factory=dict)
device_id_to_name: dict[int, str] = field(default_factory=dict)
entity_removal_callbacks: dict[EntityInfoKey, list[CALLBACK_TYPE]] = field(
default_factory=dict
Expand Down Expand Up @@ -501,19 +502,28 @@ def async_assist_satellite_config_updated(
callback_(config)

@callback
def async_register_assist_satellite_set_wake_word_callback(
def async_register_assist_satellite_set_wake_words_callback(
self,
callback_: Callable[[str], None],
callback_: Callable[[list[str]], None],
) -> CALLBACK_TYPE:
"""Register to receive callbacks when the Assist satellite's wake word is set."""
self.assist_satellite_set_wake_word_callbacks.append(callback_)
return partial(self.assist_satellite_set_wake_word_callbacks.remove, callback_)
self.assist_satellite_set_wake_words_callbacks.append(callback_)
return partial(self.assist_satellite_set_wake_words_callbacks.remove, callback_)

@callback
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
"""Notify listeners that the Assist satellite wake word has been set."""
for callback_ in self.assist_satellite_set_wake_word_callbacks.copy():
callback_(wake_word_id)
def async_assist_satellite_set_wake_word(
self, wake_word_index: int, wake_word_id: str | None
) -> None:
"""Notify listeners that the Assist satellite wake words have been set."""
if wake_word_id:
self.assist_satellite_wake_words[wake_word_index] = wake_word_id
else:
self.assist_satellite_wake_words.pop(wake_word_index, None)

wake_word_ids = list(self.assist_satellite_wake_words.values())

for callback_ in self.assist_satellite_set_wake_words_callbacks.copy():
callback_(wake_word_ids)

@callback
def async_register_entity_removal_callback(
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/esphome/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
"pipeline": {
"default": "mdi:filter-outline"
},
"pipeline_2": {
"default": "mdi:filter-outline"
},
"vad_sensitivity": {
"default": "mdi:volume-high"
},
"wake_word": {
"default": "mdi:microphone"
},
"wake_word_2": {
"default": "mdi:microphone"
}
}
}
Expand Down
Loading
Loading