mirror of
https://github.com/home-assistant/core.git
synced 2026-01-11 01:28:48 +00:00
Compare commits
2 Commits
echo
...
energy_sen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69a252092c | ||
|
|
ec257a54f3 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -324,7 +324,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@v3.8.1
|
||||
uses: sigstore/cosign-installer@v3.8.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
|
||||
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
@@ -42,14 +42,6 @@
|
||||
"--picked"
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Home Assistant: Debug Current Test File",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "pytest",
|
||||
"console": "integratedTerminal",
|
||||
"args": ["-vv", "${file}"]
|
||||
},
|
||||
{
|
||||
// Debug by attaching to local Home Assistant server using Remote Python Debugger.
|
||||
// See https://www.home-assistant.io/integrations/debugpy/
|
||||
@@ -85,4 +77,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.4.4",
|
||||
"bluetooth-data-tools==1.23.4",
|
||||
"dbus-fast==2.33.0",
|
||||
"habluetooth==3.24.0"
|
||||
"habluetooth==3.22.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ class CiscoDeviceScanner(DeviceScanner):
|
||||
"""Open connection to the router and get arp entries."""
|
||||
|
||||
try:
|
||||
cisco_ssh: pxssh.pxssh[str] = pxssh.pxssh(encoding="utf-8")
|
||||
cisco_ssh: pxssh.pxssh[str] = pxssh.pxssh(encoding="uft-8")
|
||||
cisco_ssh.login(
|
||||
self.host,
|
||||
self.username,
|
||||
|
||||
@@ -30,15 +30,10 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
[
|
||||
DemoWaterHeater(
|
||||
"Demo Water Heater", 119, UnitOfTemperature.FAHRENHEIT, False, "eco", 1
|
||||
"Demo Water Heater", 119, UnitOfTemperature.FAHRENHEIT, False, "eco"
|
||||
),
|
||||
DemoWaterHeater(
|
||||
"Demo Water Heater Celsius",
|
||||
45,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
True,
|
||||
"eco",
|
||||
1,
|
||||
"Demo Water Heater Celsius", 45, UnitOfTemperature.CELSIUS, True, "eco"
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -57,7 +52,6 @@ class DemoWaterHeater(WaterHeaterEntity):
|
||||
unit_of_measurement: str,
|
||||
away: bool,
|
||||
current_operation: str,
|
||||
target_temperature_step: float,
|
||||
) -> None:
|
||||
"""Initialize the water_heater device."""
|
||||
self._attr_name = name
|
||||
@@ -80,7 +74,6 @@ class DemoWaterHeater(WaterHeaterEntity):
|
||||
"gas",
|
||||
"off",
|
||||
]
|
||||
self._attr_target_temperature_step = target_temperature_step
|
||||
|
||||
def set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperatures."""
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.1.1",
|
||||
"aiodiscover==2.6.1",
|
||||
"cached-ipaddress==0.8.1"
|
||||
"aiodhcpwatcher==1.1.0",
|
||||
"aiodiscover==2.6.0",
|
||||
"cached-ipaddress==0.8.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.2.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.1.0"]
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ from typing import Any, Final, Literal, cast
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_LAST_RESET,
|
||||
ATTR_STATE_CLASS,
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor.recorder import reset_detected
|
||||
@@ -222,7 +222,7 @@ def _set_result_unless_done(future: asyncio.Future[None]) -> None:
|
||||
future.set_result(None)
|
||||
|
||||
|
||||
class EnergyCostSensor(SensorEntity):
|
||||
class EnergyCostSensor(RestoreSensor):
|
||||
"""Calculate costs incurred by consuming energy.
|
||||
|
||||
This is intended as a fallback for when no specific cost sensor is available for the
|
||||
@@ -312,40 +312,22 @@ class EnergyCostSensor(SensorEntity):
|
||||
return
|
||||
|
||||
# Determine energy price
|
||||
if self._config["entity_energy_price"] is not None:
|
||||
energy_price_state = self.hass.states.get(
|
||||
self._config["entity_energy_price"]
|
||||
)
|
||||
|
||||
if energy_price_state is None:
|
||||
return
|
||||
|
||||
try:
|
||||
energy_price = float(energy_price_state.state)
|
||||
except ValueError:
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities except
|
||||
# price are in place. This means that the cost will update the first
|
||||
# time the energy is updated after the price entity is in place.
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, ""
|
||||
).partition("/")[2]
|
||||
|
||||
# For backwards compatibility we don't validate the unit of the price
|
||||
# If it is not valid, we assume it's our default price unit.
|
||||
if energy_price_unit not in valid_units:
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
else:
|
||||
energy_price = cast(float, self._config["number_energy_price"])
|
||||
energy_price_unit = default_price_unit
|
||||
energy_price_tuple = self._get_energy_price(valid_units, default_price_unit)
|
||||
if energy_price_tuple is None:
|
||||
return
|
||||
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities are in place.
|
||||
self._reset(energy_state)
|
||||
# Initialize as it's the first time all required entities are in place or
|
||||
# only the price is missing. In the later case, cost will update the first
|
||||
# time the energy is updated after the price entity is in place.
|
||||
if self._attr_native_value is None:
|
||||
self._reset(energy_state)
|
||||
else:
|
||||
self._last_energy_sensor_state = energy_state
|
||||
return
|
||||
|
||||
energy_price, energy_price_unit = energy_price_tuple
|
||||
if energy_price is None:
|
||||
return
|
||||
|
||||
energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
@@ -383,20 +365,9 @@ class EnergyCostSensor(SensorEntity):
|
||||
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||
cur_value = cast(float, self._attr_native_value)
|
||||
|
||||
if energy_price_unit is None:
|
||||
converted_energy_price = energy_price
|
||||
else:
|
||||
converter: Callable[[float, str, str], float]
|
||||
if energy_unit in VALID_ENERGY_UNITS:
|
||||
converter = unit_conversion.EnergyConverter.convert
|
||||
else:
|
||||
converter = unit_conversion.VolumeConverter.convert
|
||||
|
||||
converted_energy_price = converter(
|
||||
energy_price,
|
||||
energy_unit,
|
||||
energy_price_unit,
|
||||
)
|
||||
converted_energy_price = self._convert_energy_price(
|
||||
energy_price, energy_price_unit, energy_unit
|
||||
)
|
||||
|
||||
self._attr_native_value = (
|
||||
cur_value + (energy - old_energy_value) * converted_energy_price
|
||||
@@ -404,8 +375,53 @@ class EnergyCostSensor(SensorEntity):
|
||||
|
||||
self._last_energy_sensor_state = energy_state
|
||||
|
||||
def _get_energy_price(
|
||||
self,
|
||||
valid_units: set[str],
|
||||
default_unit: str | None,
|
||||
) -> tuple[float | None, str | None] | None:
|
||||
if self._config["entity_energy_price"] is None:
|
||||
return cast(float, self._config["number_energy_price"]), default_unit
|
||||
|
||||
energy_price_state = self.hass.states.get(self._config["entity_energy_price"])
|
||||
if energy_price_state is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
energy_price = float(energy_price_state.state)
|
||||
except ValueError:
|
||||
return (None, None)
|
||||
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, ""
|
||||
).partition("/")[2]
|
||||
|
||||
# For backwards compatibility we don't validate the unit of the price
|
||||
# If it is not valid, we assume it's our default price unit.
|
||||
if energy_price_unit not in valid_units:
|
||||
energy_price_unit = default_unit
|
||||
|
||||
return energy_price, energy_price_unit
|
||||
|
||||
def _convert_energy_price(
|
||||
self, energy_price: float, energy_price_unit: str | None, energy_unit: str
|
||||
) -> float:
|
||||
if energy_price_unit is None:
|
||||
return energy_price
|
||||
|
||||
converter: Callable[[float, str, str], float]
|
||||
if energy_unit in VALID_ENERGY_UNITS:
|
||||
converter = unit_conversion.EnergyConverter.convert
|
||||
else:
|
||||
converter = unit_conversion.VolumeConverter.convert
|
||||
|
||||
return converter(energy_price, energy_unit, energy_price_unit)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
if (sensor_data := await self.async_get_last_sensor_data()) is not None:
|
||||
self._attr_native_value = sensor_data.native_value
|
||||
|
||||
energy_state = self.hass.states.get(self._config[self._adapter.stat_energy_key])
|
||||
if energy_state:
|
||||
name = energy_state.name
|
||||
|
||||
@@ -25,7 +25,6 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_MODE,
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
@@ -41,10 +40,11 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
ATTR_DURATION,
|
||||
ATTR_DURATION_DAYS,
|
||||
ATTR_DURATION_HOURS,
|
||||
ATTR_DURATION_UNTIL,
|
||||
ATTR_PERIOD,
|
||||
ATTR_SETPOINT,
|
||||
ATTR_SYSTEM_MODE,
|
||||
ATTR_ZONE_TEMP,
|
||||
CONF_LOCATION_IDX,
|
||||
DOMAIN,
|
||||
SCAN_INTERVAL_DEFAULT,
|
||||
@@ -81,7 +81,7 @@ RESET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
||||
SET_ZONE_OVERRIDE_SCHEMA: Final = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(ATTR_SETPOINT): vol.All(
|
||||
vol.Required(ATTR_ZONE_TEMP): vol.All(
|
||||
vol.Coerce(float), vol.Range(min=4.0, max=35.0)
|
||||
),
|
||||
vol.Optional(ATTR_DURATION_UNTIL): vol.All(
|
||||
@@ -222,7 +222,7 @@ def setup_service_functions(
|
||||
# Permanent-only modes will use this schema
|
||||
perm_modes = [m[SZ_SYSTEM_MODE] for m in modes if not m[SZ_CAN_BE_TEMPORARY]]
|
||||
if perm_modes: # any of: "Auto", "HeatingOff": permanent only
|
||||
schema = vol.Schema({vol.Required(ATTR_MODE): vol.In(perm_modes)})
|
||||
schema = vol.Schema({vol.Required(ATTR_SYSTEM_MODE): vol.In(perm_modes)})
|
||||
system_mode_schemas.append(schema)
|
||||
|
||||
modes = [m for m in modes if m[SZ_CAN_BE_TEMPORARY]]
|
||||
@@ -232,8 +232,8 @@ def setup_service_functions(
|
||||
if temp_modes: # any of: "AutoWithEco", permanent or for 0-24 hours
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION): vol.All(
|
||||
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION_HOURS): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(hours=0), max=timedelta(hours=24)),
|
||||
),
|
||||
@@ -246,8 +246,8 @@ def setup_service_functions(
|
||||
if temp_modes: # any of: "Away", "Custom", "DayOff", permanent or for 1-99 days
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_PERIOD): vol.All(
|
||||
vol.Required(ATTR_SYSTEM_MODE): vol.In(temp_modes),
|
||||
vol.Optional(ATTR_DURATION_DAYS): vol.All(
|
||||
cv.time_period,
|
||||
vol.Range(min=timedelta(days=1), max=timedelta(days=99)),
|
||||
),
|
||||
|
||||
@@ -29,7 +29,7 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_MODE, PRECISION_TENTHS, UnitOfTemperature
|
||||
from homeassistant.const import PRECISION_TENTHS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -38,10 +38,11 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import EVOHOME_KEY
|
||||
from .const import (
|
||||
ATTR_DURATION,
|
||||
ATTR_DURATION_DAYS,
|
||||
ATTR_DURATION_HOURS,
|
||||
ATTR_DURATION_UNTIL,
|
||||
ATTR_PERIOD,
|
||||
ATTR_SETPOINT,
|
||||
ATTR_SYSTEM_MODE,
|
||||
ATTR_ZONE_TEMP,
|
||||
EvoService,
|
||||
)
|
||||
from .coordinator import EvoDataUpdateCoordinator
|
||||
@@ -179,7 +180,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
return
|
||||
|
||||
# otherwise it is EvoService.SET_ZONE_OVERRIDE
|
||||
temperature = max(min(data[ATTR_SETPOINT], self.max_temp), self.min_temp)
|
||||
temperature = max(min(data[ATTR_ZONE_TEMP], self.max_temp), self.min_temp)
|
||||
|
||||
if ATTR_DURATION_UNTIL in data:
|
||||
duration: timedelta = data[ATTR_DURATION_UNTIL]
|
||||
@@ -348,16 +349,16 @@ class EvoController(EvoClimateEntity):
|
||||
Data validation is not required, it will have been done upstream.
|
||||
"""
|
||||
if service == EvoService.SET_SYSTEM_MODE:
|
||||
mode = data[ATTR_MODE]
|
||||
mode = data[ATTR_SYSTEM_MODE]
|
||||
else: # otherwise it is EvoService.RESET_SYSTEM
|
||||
mode = EvoSystemMode.AUTO_WITH_RESET
|
||||
|
||||
if ATTR_PERIOD in data:
|
||||
if ATTR_DURATION_DAYS in data:
|
||||
until = dt_util.start_of_local_day()
|
||||
until += data[ATTR_PERIOD]
|
||||
until += data[ATTR_DURATION_DAYS]
|
||||
|
||||
elif ATTR_DURATION in data:
|
||||
until = dt_util.now() + data[ATTR_DURATION]
|
||||
elif ATTR_DURATION_HOURS in data:
|
||||
until = dt_util.now() + data[ATTR_DURATION_HOURS]
|
||||
|
||||
else:
|
||||
until = None
|
||||
|
||||
@@ -18,10 +18,11 @@ USER_DATA: Final = "user_data"
|
||||
SCAN_INTERVAL_DEFAULT: Final = timedelta(seconds=300)
|
||||
SCAN_INTERVAL_MINIMUM: Final = timedelta(seconds=60)
|
||||
|
||||
ATTR_PERIOD: Final = "period" # number of days
|
||||
ATTR_DURATION: Final = "duration" # number of minutes, <24h
|
||||
ATTR_SYSTEM_MODE: Final = "mode"
|
||||
ATTR_DURATION_DAYS: Final = "period"
|
||||
ATTR_DURATION_HOURS: Final = "duration"
|
||||
|
||||
ATTR_SETPOINT: Final = "setpoint"
|
||||
ATTR_ZONE_TEMP: Final = "setpoint"
|
||||
ATTR_DURATION_UNTIL: Final = "duration"
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["flexit_bacnet==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfritzhome"],
|
||||
"requirements": ["pyfritzhome==0.6.17"],
|
||||
"requirements": ["pyfritzhome==0.6.15"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250221.0"]
|
||||
"requirements": ["home-assistant-frontend==20250214.0"]
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ from __future__ import annotations
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from PIL import Image
|
||||
from requests.exceptions import Timeout
|
||||
from google.ai import generativelanguage_v1beta
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import ClientError, DeadlineExceeded, GoogleAPIError
|
||||
import google.generativeai as genai
|
||||
import google.generativeai.types as genai_types
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -28,13 +29,7 @@ from homeassistant.exceptions import (
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_PROMPT,
|
||||
DOMAIN,
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
TIMEOUT_MILLIS,
|
||||
)
|
||||
from .const import CONF_CHAT_MODEL, CONF_PROMPT, DOMAIN, RECOMMENDED_CHAT_MODEL
|
||||
|
||||
SERVICE_GENERATE_CONTENT = "generate_content"
|
||||
CONF_IMAGE_FILENAME = "image_filename"
|
||||
@@ -42,8 +37,6 @@ CONF_IMAGE_FILENAME = "image_filename"
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = (Platform.CONVERSATION,)
|
||||
|
||||
type GoogleGenerativeAIConfigEntry = ConfigEntry[genai.Client]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Google Generative AI Conversation."""
|
||||
@@ -51,47 +44,42 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def generate_content(call: ServiceCall) -> ServiceResponse:
|
||||
"""Generate content from text and optionally images."""
|
||||
prompt_parts = [call.data[CONF_PROMPT]]
|
||||
image_filenames = call.data[CONF_IMAGE_FILENAME]
|
||||
for image_filename in image_filenames:
|
||||
if not hass.config.is_allowed_path(image_filename):
|
||||
raise HomeAssistantError(
|
||||
f"Cannot read `{image_filename}`, no access to path; "
|
||||
"`allowlist_external_dirs` may need to be adjusted in "
|
||||
"`configuration.yaml`"
|
||||
)
|
||||
if not Path(image_filename).exists():
|
||||
raise HomeAssistantError(f"`{image_filename}` does not exist")
|
||||
mime_type, _ = mimetypes.guess_type(image_filename)
|
||||
if mime_type is None or not mime_type.startswith("image"):
|
||||
raise HomeAssistantError(f"`{image_filename}` is not an image")
|
||||
prompt_parts.append(
|
||||
{
|
||||
"mime_type": mime_type,
|
||||
"data": await hass.async_add_executor_job(
|
||||
Path(image_filename).read_bytes
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def append_images_to_prompt():
|
||||
image_filenames = call.data[CONF_IMAGE_FILENAME]
|
||||
for image_filename in image_filenames:
|
||||
if not hass.config.is_allowed_path(image_filename):
|
||||
raise HomeAssistantError(
|
||||
f"Cannot read `{image_filename}`, no access to path; "
|
||||
"`allowlist_external_dirs` may need to be adjusted in "
|
||||
"`configuration.yaml`"
|
||||
)
|
||||
if not Path(image_filename).exists():
|
||||
raise HomeAssistantError(f"`{image_filename}` does not exist")
|
||||
mime_type, _ = mimetypes.guess_type(image_filename)
|
||||
if mime_type is None or not mime_type.startswith("image"):
|
||||
raise HomeAssistantError(f"`{image_filename}` is not an image")
|
||||
prompt_parts.append(Image.open(image_filename))
|
||||
|
||||
await hass.async_add_executor_job(append_images_to_prompt)
|
||||
|
||||
config_entry: GoogleGenerativeAIConfigEntry = hass.config_entries.async_entries(
|
||||
DOMAIN
|
||||
)[0]
|
||||
client = config_entry.runtime_data
|
||||
model = genai.GenerativeModel(model_name=RECOMMENDED_CHAT_MODEL)
|
||||
|
||||
try:
|
||||
response = await client.aio.models.generate_content(
|
||||
model=RECOMMENDED_CHAT_MODEL, contents=prompt_parts
|
||||
)
|
||||
response = await model.generate_content_async(prompt_parts)
|
||||
except (
|
||||
APIError,
|
||||
GoogleAPIError,
|
||||
ValueError,
|
||||
genai_types.BlockedPromptException,
|
||||
genai_types.StopCandidateException,
|
||||
) as err:
|
||||
raise HomeAssistantError(f"Error generating content: {err}") from err
|
||||
|
||||
if response.prompt_feedback:
|
||||
raise HomeAssistantError(
|
||||
f"Error generating content due to content violations, reason: {response.prompt_feedback.block_reason_message}"
|
||||
)
|
||||
|
||||
if not response.candidates[0].content.parts:
|
||||
raise HomeAssistantError("Unknown error generating content")
|
||||
if not response.parts:
|
||||
raise HomeAssistantError("Error generating content")
|
||||
|
||||
return {"text": response.text}
|
||||
|
||||
@@ -112,34 +100,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: GoogleGenerativeAIConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Google Generative AI Conversation from a config entry."""
|
||||
genai.configure(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
try:
|
||||
client = genai.Client(api_key=entry.data[CONF_API_KEY])
|
||||
await client.aio.models.get(
|
||||
model=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
client = generativelanguage_v1beta.ModelServiceAsyncClient(
|
||||
client_options=ClientOptions(api_key=entry.data[CONF_API_KEY])
|
||||
)
|
||||
except (APIError, Timeout) as err:
|
||||
if isinstance(err, ClientError) and "API_KEY_INVALID" in str(err):
|
||||
raise ConfigEntryAuthFailed(err.message) from err
|
||||
if isinstance(err, Timeout):
|
||||
await client.get_model(
|
||||
name=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL), timeout=5.0
|
||||
)
|
||||
except (GoogleAPIError, ValueError) as err:
|
||||
if isinstance(err, ClientError) and err.reason == "API_KEY_INVALID":
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
if isinstance(err, DeadlineExceeded):
|
||||
raise ConfigEntryNotReady(err) from err
|
||||
raise ConfigEntryError(err) from err
|
||||
else:
|
||||
entry.runtime_data = client
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GoogleGenerativeAIConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload GoogleGenerativeAI."""
|
||||
if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
return False
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from requests.exceptions import Timeout
|
||||
from google.ai import generativelanguage_v1beta
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import ClientError, GoogleAPIError
|
||||
import google.generativeai as genai
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@@ -51,7 +53,6 @@ from .const import (
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
RECOMMENDED_TOP_K,
|
||||
RECOMMENDED_TOP_P,
|
||||
TIMEOUT_MILLIS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -69,20 +70,15 @@ RECOMMENDED_OPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
async def validate_input(data: dict[str, Any]) -> None:
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
client = genai.Client(api_key=data[CONF_API_KEY])
|
||||
await client.aio.models.list(
|
||||
config={
|
||||
"http_options": {
|
||||
"timeout": TIMEOUT_MILLIS,
|
||||
},
|
||||
"query_base": True,
|
||||
}
|
||||
client = generativelanguage_v1beta.ModelServiceAsyncClient(
|
||||
client_options=ClientOptions(api_key=data[CONF_API_KEY])
|
||||
)
|
||||
await client.list_models(timeout=5.0)
|
||||
|
||||
|
||||
class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@@ -97,9 +93,9 @@ class GoogleGenerativeAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
await validate_input(user_input)
|
||||
except (APIError, Timeout) as err:
|
||||
if isinstance(err, ClientError) and "API_KEY_INVALID" in str(err):
|
||||
await validate_input(self.hass, user_input)
|
||||
except GoogleAPIError as err:
|
||||
if isinstance(err, ClientError) and err.reason == "API_KEY_INVALID":
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
@@ -170,7 +166,6 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
self.last_rendered_recommended = config_entry.options.get(
|
||||
CONF_RECOMMENDED, False
|
||||
)
|
||||
self._genai_client = config_entry.runtime_data
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -193,9 +188,7 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
||||
}
|
||||
|
||||
schema = await google_generative_ai_config_option_schema(
|
||||
self.hass, options, self._genai_client
|
||||
)
|
||||
schema = await google_generative_ai_config_option_schema(self.hass, options)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(schema),
|
||||
@@ -205,7 +198,6 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
async def google_generative_ai_config_option_schema(
|
||||
hass: HomeAssistant,
|
||||
options: dict[str, Any] | MappingProxyType[str, Any],
|
||||
genai_client: genai.Client,
|
||||
) -> dict:
|
||||
"""Return a schema for Google Generative AI completion options."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
@@ -244,21 +236,18 @@ async def google_generative_ai_config_option_schema(
|
||||
if options.get(CONF_RECOMMENDED):
|
||||
return schema
|
||||
|
||||
api_models_pager = await genai_client.aio.models.list(config={"query_base": True})
|
||||
api_models = [api_model async for api_model in api_models_pager]
|
||||
api_models = await hass.async_add_executor_job(partial(genai.list_models))
|
||||
|
||||
models = [
|
||||
SelectOptionDict(
|
||||
label=api_model.display_name,
|
||||
value=api_model.name,
|
||||
)
|
||||
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
|
||||
for api_model in sorted(api_models, key=lambda x: x.display_name)
|
||||
if (
|
||||
api_model.name != "models/gemini-1.0-pro" # duplicate of gemini-pro
|
||||
and api_model.display_name
|
||||
and api_model.name
|
||||
and api_model.supported_actions
|
||||
and "vision" not in api_model.name
|
||||
and "generateContent" in api_model.supported_actions
|
||||
and "generateContent" in api_model.supported_generation_methods
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -22,5 +22,3 @@ CONF_HATE_BLOCK_THRESHOLD = "hate_block_threshold"
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD = "sexual_block_threshold"
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD = "dangerous_block_threshold"
|
||||
RECOMMENDED_HARM_BLOCK_THRESHOLD = "BLOCK_MEDIUM_AND_ABOVE"
|
||||
|
||||
TIMEOUT_MILLIS = 10000
|
||||
|
||||
@@ -6,18 +6,11 @@ import codecs
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
from google.genai.errors import APIError
|
||||
from google.genai.types import (
|
||||
AutomaticFunctionCallingConfig,
|
||||
Content,
|
||||
FunctionDeclaration,
|
||||
GenerateContentConfig,
|
||||
HarmCategory,
|
||||
Part,
|
||||
SafetySetting,
|
||||
Schema,
|
||||
Tool,
|
||||
)
|
||||
from google.api_core.exceptions import GoogleAPIError
|
||||
import google.generativeai as genai
|
||||
from google.generativeai import protos
|
||||
import google.generativeai.types as genai_types
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
from homeassistant.components import assist_pipeline, conversation
|
||||
@@ -64,40 +57,21 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
SUPPORTED_SCHEMA_KEYS = {
|
||||
"min_items",
|
||||
"example",
|
||||
"property_ordering",
|
||||
"pattern",
|
||||
"minimum",
|
||||
"default",
|
||||
"any_of",
|
||||
"max_length",
|
||||
"title",
|
||||
"min_properties",
|
||||
"min_length",
|
||||
"max_items",
|
||||
"maximum",
|
||||
"nullable",
|
||||
"max_properties",
|
||||
"type",
|
||||
"description",
|
||||
"enum",
|
||||
"format",
|
||||
"description",
|
||||
"nullable",
|
||||
"enum",
|
||||
"items",
|
||||
"properties",
|
||||
"required",
|
||||
}
|
||||
|
||||
|
||||
def _camel_to_snake(name: str) -> str:
|
||||
"""Convert camel case to snake case."""
|
||||
return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_")
|
||||
|
||||
|
||||
def _format_schema(schema: dict[str, Any]) -> Schema:
|
||||
"""Format the schema to be compatible with Gemini API."""
|
||||
if subschemas := schema.get("allOf"):
|
||||
for subschema in subschemas: # Gemini API does not support allOf keys
|
||||
def _format_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Format the schema to protobuf."""
|
||||
if (subschemas := schema.get("anyOf")) or (subschemas := schema.get("allOf")):
|
||||
for subschema in subschemas: # Gemini API does not support anyOf and allOf keys
|
||||
if "type" in subschema: # Fallback to first subschema with 'type' field
|
||||
return _format_schema(subschema)
|
||||
return _format_schema(
|
||||
@@ -106,38 +80,42 @@ def _format_schema(schema: dict[str, Any]) -> Schema:
|
||||
|
||||
result = {}
|
||||
for key, val in schema.items():
|
||||
key = _camel_to_snake(key)
|
||||
if key not in SUPPORTED_SCHEMA_KEYS:
|
||||
continue
|
||||
if key == "any_of":
|
||||
val = [_format_schema(subschema) for subschema in val]
|
||||
if key == "type":
|
||||
key = "type_"
|
||||
val = val.upper()
|
||||
if key == "items":
|
||||
elif key == "format":
|
||||
if schema.get("type") == "string" and val != "enum":
|
||||
continue
|
||||
if schema.get("type") not in ("number", "integer", "string"):
|
||||
continue
|
||||
key = "format_"
|
||||
elif key == "items":
|
||||
val = _format_schema(val)
|
||||
elif key == "properties":
|
||||
val = {k: _format_schema(v) for k, v in val.items()}
|
||||
result[key] = val
|
||||
|
||||
if result.get("enum") and result.get("type") != "STRING":
|
||||
if result.get("enum") and result.get("type_") != "STRING":
|
||||
# enum is only allowed for STRING type. This is safe as long as the schema
|
||||
# contains vol.Coerce for the respective type, for example:
|
||||
# vol.All(vol.Coerce(int), vol.In([1, 2, 3]))
|
||||
result["type"] = "STRING"
|
||||
result["type_"] = "STRING"
|
||||
result["enum"] = [str(item) for item in result["enum"]]
|
||||
|
||||
if result.get("type") == "OBJECT" and not result.get("properties"):
|
||||
if result.get("type_") == "OBJECT" and not result.get("properties"):
|
||||
# An object with undefined properties is not supported by Gemini API.
|
||||
# Fallback to JSON string. This will probably fail for most tools that want it,
|
||||
# but we don't have a better fallback strategy so far.
|
||||
result["properties"] = {"json": {"type": "STRING"}}
|
||||
result["properties"] = {"json": {"type_": "STRING"}}
|
||||
result["required"] = []
|
||||
return cast(Schema, result)
|
||||
return result
|
||||
|
||||
|
||||
def _format_tool(
|
||||
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
|
||||
) -> Tool:
|
||||
) -> dict[str, Any]:
|
||||
"""Format tool specification."""
|
||||
|
||||
if tool.parameters.schema:
|
||||
@@ -147,14 +125,16 @@ def _format_tool(
|
||||
else:
|
||||
parameters = None
|
||||
|
||||
return Tool(
|
||||
function_declarations=[
|
||||
FunctionDeclaration(
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
parameters=parameters,
|
||||
)
|
||||
]
|
||||
return protos.Tool(
|
||||
{
|
||||
"function_declarations": [
|
||||
{
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": parameters,
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -171,12 +151,14 @@ def _escape_decode(value: Any) -> Any:
|
||||
|
||||
def _create_google_tool_response_content(
|
||||
content: list[conversation.ToolResultContent],
|
||||
) -> Content:
|
||||
) -> protos.Content:
|
||||
"""Create a Google tool response content."""
|
||||
return Content(
|
||||
return protos.Content(
|
||||
parts=[
|
||||
Part.from_function_response(
|
||||
name=tool_result.tool_name, response=tool_result.tool_result
|
||||
protos.Part(
|
||||
function_response=protos.FunctionResponse(
|
||||
name=tool_result.tool_name, response=tool_result.tool_result
|
||||
)
|
||||
)
|
||||
for tool_result in content
|
||||
]
|
||||
@@ -187,36 +169,33 @@ def _convert_content(
|
||||
content: conversation.UserContent
|
||||
| conversation.AssistantContent
|
||||
| conversation.SystemContent,
|
||||
) -> Content:
|
||||
) -> genai_types.ContentDict:
|
||||
"""Convert HA content to Google content."""
|
||||
if content.role != "assistant" or not content.tool_calls: # type: ignore[union-attr]
|
||||
role = "model" if content.role == "assistant" else content.role
|
||||
return Content(
|
||||
role=role,
|
||||
parts=[
|
||||
Part.from_text(text=content.content if content.content else ""),
|
||||
],
|
||||
)
|
||||
return {"role": role, "parts": content.content}
|
||||
|
||||
# Handle the Assistant content with tool calls.
|
||||
assert type(content) is conversation.AssistantContent
|
||||
parts: list[Part] = []
|
||||
parts = []
|
||||
|
||||
if content.content:
|
||||
parts.append(Part.from_text(text=content.content))
|
||||
parts.append(protos.Part(text=content.content))
|
||||
|
||||
if content.tool_calls:
|
||||
parts.extend(
|
||||
[
|
||||
Part.from_function_call(
|
||||
name=tool_call.tool_name,
|
||||
args=_escape_decode(tool_call.tool_args),
|
||||
protos.Part(
|
||||
function_call=protos.FunctionCall(
|
||||
name=tool_call.tool_name,
|
||||
args=_escape_decode(tool_call.tool_args),
|
||||
)
|
||||
)
|
||||
for tool_call in content.tool_calls
|
||||
]
|
||||
)
|
||||
|
||||
return Content(role="model", parts=parts)
|
||||
return protos.Content({"role": "model", "parts": parts})
|
||||
|
||||
|
||||
class GoogleGenerativeAIConversationEntity(
|
||||
@@ -230,7 +209,6 @@ class GoogleGenerativeAIConversationEntity(
|
||||
def __init__(self, entry: ConfigEntry) -> None:
|
||||
"""Initialize the agent."""
|
||||
self.entry = entry
|
||||
self._genai_client = entry.runtime_data
|
||||
self._attr_unique_id = entry.entry_id
|
||||
self._attr_device_info = dr.DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.entry_id)},
|
||||
@@ -295,7 +273,7 @@ class GoogleGenerativeAIConversationEntity(
|
||||
except conversation.ConverseError as err:
|
||||
return err.as_conversation_result()
|
||||
|
||||
tools: list[Tool | Callable[..., Any]] | None = None
|
||||
tools: list[dict[str, Any]] | None = None
|
||||
if chat_log.llm_api:
|
||||
tools = [
|
||||
_format_tool(tool, chat_log.llm_api.custom_serializer)
|
||||
@@ -310,22 +288,13 @@ class GoogleGenerativeAIConversationEntity(
|
||||
"gemini-1.0" not in model_name and "gemini-pro" not in model_name
|
||||
)
|
||||
|
||||
prompt_content = cast(
|
||||
conversation.SystemContent,
|
||||
chat_log.content[0],
|
||||
)
|
||||
|
||||
if prompt_content.content:
|
||||
prompt = prompt_content.content
|
||||
else:
|
||||
raise HomeAssistantError("Invalid prompt content")
|
||||
|
||||
messages: list[Content] = []
|
||||
prompt = chat_log.content[0].content # type: ignore[union-attr]
|
||||
messages: list[genai_types.ContentDict] = []
|
||||
|
||||
# Google groups tool results, we do not. Group them before sending.
|
||||
tool_results: list[conversation.ToolResultContent] = []
|
||||
|
||||
for chat_content in chat_log.content[1:-1]:
|
||||
for chat_content in chat_log.content[1:]:
|
||||
if chat_content.role == "tool_result":
|
||||
# mypy doesn't like picking a type based on checking shared property 'role'
|
||||
tool_results.append(cast(conversation.ToolResultContent, chat_content))
|
||||
@@ -348,93 +317,85 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
if tool_results:
|
||||
messages.append(_create_google_tool_response_content(tool_results))
|
||||
generateContentConfig = GenerateContentConfig(
|
||||
temperature=self.entry.options.get(
|
||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||
),
|
||||
top_k=self.entry.options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
|
||||
top_p=self.entry.options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
|
||||
max_output_tokens=self.entry.options.get(
|
||||
CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS
|
||||
),
|
||||
safety_settings=[
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_HATE_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
|
||||
model = genai.GenerativeModel(
|
||||
model_name=model_name,
|
||||
generation_config={
|
||||
"temperature": self.entry.options.get(
|
||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_HARASSMENT,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD,
|
||||
RECOMMENDED_HARM_BLOCK_THRESHOLD,
|
||||
),
|
||||
"top_p": self.entry.options.get(CONF_TOP_P, RECOMMENDED_TOP_P),
|
||||
"top_k": self.entry.options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
|
||||
"max_output_tokens": self.entry.options.get(
|
||||
CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
},
|
||||
safety_settings={
|
||||
"HARASSMENT": self.entry.options.get(
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
SafetySetting(
|
||||
category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
|
||||
threshold=self.entry.options.get(
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
"HATE": self.entry.options.get(
|
||||
CONF_HATE_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
],
|
||||
"SEXUAL": self.entry.options.get(
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
"DANGEROUS": self.entry.options.get(
|
||||
CONF_DANGEROUS_BLOCK_THRESHOLD, RECOMMENDED_HARM_BLOCK_THRESHOLD
|
||||
),
|
||||
},
|
||||
tools=tools or None,
|
||||
system_instruction=prompt if supports_system_instruction else None,
|
||||
automatic_function_calling=AutomaticFunctionCallingConfig(
|
||||
disable=True, maximum_remote_calls=None
|
||||
),
|
||||
)
|
||||
|
||||
if not supports_system_instruction:
|
||||
messages = [
|
||||
Content(role="user", parts=[Part.from_text(text=prompt)]),
|
||||
Content(role="model", parts=[Part.from_text(text="Ok")]),
|
||||
{"role": "user", "parts": prompt},
|
||||
{"role": "model", "parts": "Ok"},
|
||||
*messages,
|
||||
]
|
||||
chat = self._genai_client.aio.chats.create(
|
||||
model=model_name, history=messages, config=generateContentConfig
|
||||
)
|
||||
chat_request: str | Content = user_input.text
|
||||
|
||||
chat = model.start_chat(history=messages)
|
||||
chat_request = user_input.text
|
||||
# To prevent infinite loops, we limit the number of iterations
|
||||
for _iteration in range(MAX_TOOL_ITERATIONS):
|
||||
try:
|
||||
chat_response = await chat.send_message(message=chat_request)
|
||||
|
||||
if chat_response.prompt_feedback:
|
||||
raise HomeAssistantError(
|
||||
f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}"
|
||||
)
|
||||
|
||||
chat_response = await chat.send_message_async(chat_request)
|
||||
except (
|
||||
APIError,
|
||||
GoogleAPIError,
|
||||
ValueError,
|
||||
genai_types.BlockedPromptException,
|
||||
genai_types.StopCandidateException,
|
||||
) as err:
|
||||
LOGGER.error("Error sending message: %s %s", type(err), err)
|
||||
error = f"Sorry, I had a problem talking to Google Generative AI: {err}"
|
||||
|
||||
if isinstance(
|
||||
err, genai_types.StopCandidateException
|
||||
) and "finish_reason: SAFETY\n" in str(err):
|
||||
error = "The message got blocked by your safety settings"
|
||||
else:
|
||||
error = (
|
||||
f"Sorry, I had a problem talking to Google Generative AI: {err}"
|
||||
)
|
||||
|
||||
raise HomeAssistantError(error) from err
|
||||
|
||||
response_parts = chat_response.candidates[0].content.parts
|
||||
if not response_parts:
|
||||
LOGGER.debug("Response: %s", chat_response.parts)
|
||||
if not chat_response.parts:
|
||||
raise HomeAssistantError(
|
||||
"Sorry, I had a problem getting a response from Google Generative AI."
|
||||
)
|
||||
content = " ".join(
|
||||
[part.text.strip() for part in response_parts if part.text]
|
||||
[part.text.strip() for part in chat_response.parts if part.text]
|
||||
)
|
||||
|
||||
tool_calls = []
|
||||
for part in response_parts:
|
||||
for part in chat_response.parts:
|
||||
if not part.function_call:
|
||||
continue
|
||||
tool_call = part.function_call
|
||||
tool_name = tool_call.name
|
||||
tool_args = _escape_decode(tool_call.args)
|
||||
tool_call = MessageToDict(part.function_call._pb) # noqa: SLF001
|
||||
tool_name = tool_call["name"]
|
||||
tool_args = _escape_decode(tool_call["args"])
|
||||
tool_calls.append(
|
||||
llm.ToolInput(tool_name=tool_name, tool_args=tool_args)
|
||||
)
|
||||
@@ -457,7 +418,7 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
response = intent.IntentResponse(language=user_input.language)
|
||||
response.async_set_speech(
|
||||
" ".join([part.text.strip() for part in response_parts if part.text])
|
||||
" ".join([part.text.strip() for part in chat_response.parts if part.text])
|
||||
)
|
||||
return conversation.ConversationResult(
|
||||
response=response, conversation_id=chat_log.conversation_id
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["google-genai==1.1.0"]
|
||||
"requirements": ["google-generativeai==0.8.2"]
|
||||
}
|
||||
|
||||
@@ -119,13 +119,12 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
assert self.todo_items
|
||||
|
||||
if previous_uid:
|
||||
pos = self.todo_items.index(
|
||||
next(item for item in self.todo_items if item.uid == previous_uid)
|
||||
pos = (
|
||||
self.todo_items.index(
|
||||
next(item for item in self.todo_items if item.uid == previous_uid)
|
||||
)
|
||||
+ 1
|
||||
)
|
||||
if pos < self.todo_items.index(
|
||||
next(item for item in self.todo_items if item.uid == uid)
|
||||
):
|
||||
pos += 1
|
||||
else:
|
||||
pos = 0
|
||||
|
||||
|
||||
@@ -102,18 +102,6 @@ async def _validate_auth(
|
||||
return True
|
||||
|
||||
|
||||
def _get_current_hosts(entry: HeosConfigEntry) -> set[str]:
|
||||
"""Get a set of current hosts from the entry."""
|
||||
hosts = set(entry.data[CONF_HOST])
|
||||
if hasattr(entry, "runtime_data"):
|
||||
hosts.update(
|
||||
player.ip_address
|
||||
for player in entry.runtime_data.heos.players.values()
|
||||
if player.ip_address is not None
|
||||
)
|
||||
return hosts
|
||||
|
||||
|
||||
class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Define a flow for HEOS."""
|
||||
|
||||
@@ -137,15 +125,10 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if TYPE_CHECKING:
|
||||
assert discovery_info.ssdp_location
|
||||
|
||||
entry: HeosConfigEntry | None = await self.async_set_unique_id(DOMAIN)
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
# Connect to discovered host and get system information
|
||||
hostname = urlparse(discovery_info.ssdp_location).hostname
|
||||
assert hostname is not None
|
||||
|
||||
# Abort early when discovered host is part of the current system
|
||||
if entry and hostname in _get_current_hosts(entry):
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
# Connect to discovered host and get system information
|
||||
heos = Heos(HeosOptions(hostname, events=False, heart_beat=False))
|
||||
try:
|
||||
await heos.connect()
|
||||
@@ -163,23 +146,8 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
# Select the preferred host, if available
|
||||
if system_info.preferred_hosts:
|
||||
hostname = system_info.preferred_hosts[0].ip_address
|
||||
|
||||
# Move to confirmation when not configured
|
||||
if entry is None:
|
||||
self._discovered_host = hostname
|
||||
return await self.async_step_confirm_discovery()
|
||||
|
||||
# Only update if the configured host isn't part of the discovered hosts to ensure new players that come online don't trigger a reload
|
||||
if entry.data[CONF_HOST] not in [host.ip_address for host in system_info.hosts]:
|
||||
_LOGGER.debug(
|
||||
"Updated host %s to discovered host %s", entry.data[CONF_HOST], hostname
|
||||
)
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data_updates={CONF_HOST: hostname},
|
||||
reason="reconfigure_successful",
|
||||
)
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
self._discovered_host = hostname
|
||||
return await self.async_step_confirm_discovery()
|
||||
|
||||
async def async_step_confirm_discovery(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -199,7 +167,6 @@ class HeosFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Obtain host and validate connection."""
|
||||
await self.async_set_unique_id(DOMAIN)
|
||||
self._abort_if_unique_id_configured(error="single_instance_allowed")
|
||||
# Try connecting to host if provided
|
||||
errors: dict[str, str] = {}
|
||||
host = None
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyheos"],
|
||||
"quality_scale": "platinum",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyheos==1.0.2"],
|
||||
"single_config_entry": true,
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
||||
|
||||
@@ -38,7 +38,9 @@ rules:
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: Explore if this is possible.
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.67", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.66", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -187,7 +187,6 @@ SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
"""Provides button entities for Home Connect."""
|
||||
|
||||
from aiohomeconnect.model import CommandKey, EventKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
|
||||
class HomeConnectCommandButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Home Connect button entity."""
|
||||
|
||||
key: CommandKey
|
||||
|
||||
|
||||
COMMAND_BUTTONS = (
|
||||
HomeConnectCommandButtonEntityDescription(
|
||||
key=CommandKey.BSH_COMMON_OPEN_DOOR,
|
||||
translation_key="open_door",
|
||||
),
|
||||
HomeConnectCommandButtonEntityDescription(
|
||||
key=CommandKey.BSH_COMMON_PARTLY_OPEN_DOOR,
|
||||
translation_key="partly_open_door",
|
||||
),
|
||||
HomeConnectCommandButtonEntityDescription(
|
||||
key=CommandKey.BSH_COMMON_PAUSE_PROGRAM,
|
||||
translation_key="pause_program",
|
||||
),
|
||||
HomeConnectCommandButtonEntityDescription(
|
||||
key=CommandKey.BSH_COMMON_RESUME_PROGRAM,
|
||||
translation_key="resume_program",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> list[HomeConnectEntity]:
|
||||
"""Get a list of entities."""
|
||||
entities: list[HomeConnectEntity] = []
|
||||
entities.extend(
|
||||
HomeConnectCommandButtonEntity(entry.runtime_data, appliance, description)
|
||||
for description in COMMAND_BUTTONS
|
||||
if description.key in appliance.commands
|
||||
)
|
||||
if appliance.info.type in APPLIANCES_WITH_PROGRAMS:
|
||||
entities.append(
|
||||
HomeConnectStopProgramButtonEntity(entry.runtime_data, appliance)
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Home Connect button entities."""
|
||||
setup_home_connect_entry(
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectButtonEntity(HomeConnectEntity, ButtonEntity):
|
||||
"""Describes Home Connect button entity."""
|
||||
|
||||
entity_description: ButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
desc: ButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
# The entity is subscribed to the appliance connected event,
|
||||
# but it will receive also the disconnected event
|
||||
ButtonEntityDescription(
|
||||
key=EventKey.BSH_COMMON_APPLIANCE_CONNECTED,
|
||||
),
|
||||
)
|
||||
self.entity_description = desc
|
||||
self.appliance = appliance
|
||||
self.unique_id = f"{appliance.info.ha_id}-{desc.key}"
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
|
||||
|
||||
class HomeConnectCommandButtonEntity(HomeConnectButtonEntity):
|
||||
"""Button entity for Home Connect commands."""
|
||||
|
||||
entity_description: HomeConnectCommandButtonEntityDescription
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
try:
|
||||
await self.coordinator.client.put_command(
|
||||
self.appliance.info.ha_id,
|
||||
command_key=self.entity_description.key,
|
||||
value=True,
|
||||
)
|
||||
except HomeConnectError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="execute_command",
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(error),
|
||||
"command": self.entity_description.key,
|
||||
},
|
||||
) from error
|
||||
|
||||
|
||||
class HomeConnectStopProgramButtonEntity(HomeConnectButtonEntity):
|
||||
"""Button entity for stopping a program."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
ButtonEntityDescription(
|
||||
key="StopProgram",
|
||||
translation_key="stop_program",
|
||||
),
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
try:
|
||||
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
|
||||
except HomeConnectError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="stop_program",
|
||||
translation_placeholders=get_dict_from_home_connect_error(error),
|
||||
) from error
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Common callbacks for all Home Connect platforms."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from functools import partial
|
||||
from typing import cast
|
||||
@@ -10,32 +9,7 @@ from aiohomeconnect.model import EventKey
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
|
||||
|
||||
def _create_option_entities(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
known_entity_unique_ids: dict[str, str],
|
||||
get_option_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData],
|
||||
list[HomeConnectOptionEntity],
|
||||
],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create the required option entities for the appliances."""
|
||||
option_entities_to_add = [
|
||||
entity
|
||||
for entity in get_option_entities_for_appliance(entry, appliance)
|
||||
if entity.unique_id not in known_entity_unique_ids
|
||||
]
|
||||
known_entity_unique_ids.update(
|
||||
{
|
||||
cast(str, entity.unique_id): appliance.info.ha_id
|
||||
for entity in option_entities_to_add
|
||||
}
|
||||
)
|
||||
async_add_entities(option_entities_to_add)
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
|
||||
def _handle_paired_or_connected_appliance(
|
||||
@@ -44,12 +18,6 @@ def _handle_paired_or_connected_appliance(
|
||||
get_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData], list[HomeConnectEntity]
|
||||
],
|
||||
get_option_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData],
|
||||
list[HomeConnectOptionEntity],
|
||||
]
|
||||
| None,
|
||||
changed_options_listener_remove_callbacks: dict[str, list[Callable[[], None]]],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Handle a new paired appliance or an appliance that has been connected.
|
||||
@@ -66,28 +34,6 @@ def _handle_paired_or_connected_appliance(
|
||||
for entity in get_entities_for_appliance(entry, appliance)
|
||||
if entity.unique_id not in known_entity_unique_ids
|
||||
]
|
||||
if get_option_entities_for_appliance:
|
||||
entities_to_add.extend(
|
||||
entity
|
||||
for entity in get_option_entities_for_appliance(entry, appliance)
|
||||
if entity.unique_id not in known_entity_unique_ids
|
||||
)
|
||||
changed_options_listener_remove_callback = (
|
||||
entry.runtime_data.async_add_listener(
|
||||
partial(
|
||||
_create_option_entities,
|
||||
entry,
|
||||
appliance,
|
||||
known_entity_unique_ids,
|
||||
get_option_entities_for_appliance,
|
||||
async_add_entities,
|
||||
),
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(changed_options_listener_remove_callback)
|
||||
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
|
||||
changed_options_listener_remove_callback
|
||||
)
|
||||
known_entity_unique_ids.update(
|
||||
{
|
||||
cast(str, entity.unique_id): appliance.info.ha_id
|
||||
@@ -101,17 +47,11 @@ def _handle_paired_or_connected_appliance(
|
||||
def _handle_depaired_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
known_entity_unique_ids: dict[str, str],
|
||||
changed_options_listener_remove_callbacks: dict[str, list[Callable[[], None]]],
|
||||
) -> None:
|
||||
"""Handle a removed appliance."""
|
||||
for entity_unique_id, appliance_id in known_entity_unique_ids.copy().items():
|
||||
if appliance_id not in entry.runtime_data.data:
|
||||
known_entity_unique_ids.pop(entity_unique_id, None)
|
||||
if appliance_id in changed_options_listener_remove_callbacks:
|
||||
for listener in changed_options_listener_remove_callbacks.pop(
|
||||
appliance_id
|
||||
):
|
||||
listener()
|
||||
|
||||
|
||||
def setup_home_connect_entry(
|
||||
@@ -120,44 +60,13 @@ def setup_home_connect_entry(
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData], list[HomeConnectEntity]
|
||||
],
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
get_option_entities_for_appliance: Callable[
|
||||
[HomeConnectConfigEntry, HomeConnectApplianceData],
|
||||
list[HomeConnectOptionEntity],
|
||||
]
|
||||
| None = None,
|
||||
) -> None:
|
||||
"""Set up the callbacks for paired and depaired appliances."""
|
||||
known_entity_unique_ids: dict[str, str] = {}
|
||||
changed_options_listener_remove_callbacks: dict[str, list[Callable[[], None]]] = (
|
||||
defaultdict(list)
|
||||
)
|
||||
|
||||
entities: list[HomeConnectEntity] = []
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
entities_to_add = get_entities_for_appliance(entry, appliance)
|
||||
if get_option_entities_for_appliance:
|
||||
entities_to_add.extend(get_option_entities_for_appliance(entry, appliance))
|
||||
for event_key in (
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
):
|
||||
changed_options_listener_remove_callback = (
|
||||
entry.runtime_data.async_add_listener(
|
||||
partial(
|
||||
_create_option_entities,
|
||||
entry,
|
||||
appliance,
|
||||
known_entity_unique_ids,
|
||||
get_option_entities_for_appliance,
|
||||
async_add_entities,
|
||||
),
|
||||
(appliance.info.ha_id, event_key),
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(changed_options_listener_remove_callback)
|
||||
changed_options_listener_remove_callbacks[appliance.info.ha_id].append(
|
||||
changed_options_listener_remove_callback
|
||||
)
|
||||
known_entity_unique_ids.update(
|
||||
{
|
||||
cast(str, entity.unique_id): appliance.info.ha_id
|
||||
@@ -174,8 +83,6 @@ def setup_home_connect_entry(
|
||||
entry,
|
||||
known_entity_unique_ids,
|
||||
get_entities_for_appliance,
|
||||
get_option_entities_for_appliance,
|
||||
changed_options_listener_remove_callbacks,
|
||||
async_add_entities,
|
||||
),
|
||||
(
|
||||
@@ -186,12 +93,7 @@ def setup_home_connect_entry(
|
||||
)
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_add_special_listener(
|
||||
partial(
|
||||
_handle_depaired_appliance,
|
||||
entry,
|
||||
known_entity_unique_ids,
|
||||
changed_options_listener_remove_callbacks,
|
||||
),
|
||||
partial(_handle_depaired_appliance, entry, known_entity_unique_ids),
|
||||
(EventKey.BSH_COMMON_APPLIANCE_DEPAIRED,),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
|
||||
}
|
||||
|
||||
AVAILABLE_MAPS_ENUM = {
|
||||
REFERENCE_MAP_ID_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.TempMap",
|
||||
@@ -305,7 +305,7 @@ PROGRAM_ENUM_OPTIONS = {
|
||||
for option_key, options in (
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_REFERENCE_MAP_ID,
|
||||
AVAILABLE_MAPS_ENUM,
|
||||
REFERENCE_MAP_ID_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_MODE,
|
||||
|
||||
@@ -7,19 +7,16 @@ from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import (
|
||||
CommandKey,
|
||||
Event,
|
||||
EventKey,
|
||||
EventMessage,
|
||||
EventType,
|
||||
GetSetting,
|
||||
HomeAppliance,
|
||||
OptionKey,
|
||||
ProgramKey,
|
||||
SettingKey,
|
||||
Status,
|
||||
StatusKey,
|
||||
@@ -31,7 +28,7 @@ from aiohomeconnect.model.error import (
|
||||
HomeConnectRequestError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from aiohomeconnect.model.program import EnumerateProgram, ProgramDefinitionOption
|
||||
from aiohomeconnect.model.program import EnumerateProgram
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -54,21 +51,16 @@ EVENT_STREAM_RECONNECT_DELAY = 30
|
||||
class HomeConnectApplianceData:
|
||||
"""Class to hold Home Connect appliance data."""
|
||||
|
||||
commands: set[CommandKey]
|
||||
events: dict[EventKey, Event]
|
||||
info: HomeAppliance
|
||||
options: dict[OptionKey, ProgramDefinitionOption]
|
||||
programs: list[EnumerateProgram]
|
||||
settings: dict[SettingKey, GetSetting]
|
||||
status: dict[StatusKey, Status]
|
||||
|
||||
def update(self, other: HomeConnectApplianceData) -> None:
|
||||
"""Update data with data from other instance."""
|
||||
self.commands.update(other.commands)
|
||||
self.events.update(other.events)
|
||||
self.info.connected = other.info.connected
|
||||
self.options.clear()
|
||||
self.options.update(other.options)
|
||||
self.programs.clear()
|
||||
self.programs.extend(other.programs)
|
||||
self.settings.update(other.settings)
|
||||
@@ -180,9 +172,8 @@ class HomeConnectCoordinator(
|
||||
settings = self.data[event_message_ha_id].settings
|
||||
events = self.data[event_message_ha_id].events
|
||||
for event in event_message.data.items:
|
||||
event_key = event.key
|
||||
if event_key in SettingKey:
|
||||
setting_key = SettingKey(event_key)
|
||||
if event.key in SettingKey:
|
||||
setting_key = SettingKey(event.key)
|
||||
if setting_key in settings:
|
||||
settings[setting_key].value = event.value
|
||||
else:
|
||||
@@ -192,16 +183,7 @@ class HomeConnectCoordinator(
|
||||
value=event.value,
|
||||
)
|
||||
else:
|
||||
if event_key in (
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
):
|
||||
await self.update_options(
|
||||
event_message_ha_id,
|
||||
event_key,
|
||||
ProgramKey(cast(str, event.value)),
|
||||
)
|
||||
events[event_key] = event
|
||||
events[event.key] = event
|
||||
self._call_event_listener(event_message)
|
||||
|
||||
case EventType.EVENT:
|
||||
@@ -356,7 +338,6 @@ class HomeConnectCoordinator(
|
||||
|
||||
programs = []
|
||||
events = {}
|
||||
options = {}
|
||||
if appliance.type in APPLIANCES_WITH_PROGRAMS:
|
||||
try:
|
||||
all_programs = await self.client.get_all_programs(appliance.ha_id)
|
||||
@@ -370,17 +351,15 @@ class HomeConnectCoordinator(
|
||||
)
|
||||
else:
|
||||
programs.extend(all_programs.programs)
|
||||
current_program_key = None
|
||||
program_options = None
|
||||
for program, event_key in (
|
||||
(
|
||||
all_programs.selected,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
),
|
||||
(
|
||||
all_programs.active,
|
||||
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
),
|
||||
(
|
||||
all_programs.selected,
|
||||
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
|
||||
),
|
||||
):
|
||||
if program and program.key:
|
||||
events[event_key] = Event(
|
||||
@@ -391,41 +370,10 @@ class HomeConnectCoordinator(
|
||||
"",
|
||||
program.key,
|
||||
)
|
||||
current_program_key = program.key
|
||||
program_options = program.options
|
||||
if current_program_key:
|
||||
options = await self.get_options_definitions(
|
||||
appliance.ha_id, current_program_key
|
||||
)
|
||||
for option in program_options or []:
|
||||
option_event_key = EventKey(option.key)
|
||||
events[option_event_key] = Event(
|
||||
option_event_key,
|
||||
option.key,
|
||||
0,
|
||||
"",
|
||||
"",
|
||||
option.value,
|
||||
option.name,
|
||||
display_value=option.display_value,
|
||||
unit=option.unit,
|
||||
)
|
||||
|
||||
try:
|
||||
commands = {
|
||||
command.key
|
||||
for command in (
|
||||
await self.client.get_available_commands(appliance.ha_id)
|
||||
).commands
|
||||
}
|
||||
except HomeConnectError:
|
||||
commands = set()
|
||||
|
||||
appliance_data = HomeConnectApplianceData(
|
||||
commands=commands,
|
||||
events=events,
|
||||
info=appliance,
|
||||
options=options,
|
||||
programs=programs,
|
||||
settings=settings,
|
||||
status=status,
|
||||
@@ -435,48 +383,3 @@ class HomeConnectCoordinator(
|
||||
appliance_data = appliance_data_to_update
|
||||
|
||||
return appliance_data
|
||||
|
||||
async def get_options_definitions(
|
||||
self, ha_id: str, program_key: ProgramKey
|
||||
) -> dict[OptionKey, ProgramDefinitionOption]:
|
||||
"""Get options with constraints for appliance."""
|
||||
return {
|
||||
option.key: option
|
||||
for option in (
|
||||
await self.client.get_available_program(ha_id, program_key=program_key)
|
||||
).options
|
||||
or []
|
||||
}
|
||||
|
||||
async def update_options(
|
||||
self, ha_id: str, event_key: EventKey, program_key: ProgramKey
|
||||
) -> None:
|
||||
"""Update options for appliance."""
|
||||
options = self.data[ha_id].options
|
||||
events = self.data[ha_id].events
|
||||
options_to_notify = options.copy()
|
||||
options.clear()
|
||||
if program_key is not ProgramKey.UNKNOWN:
|
||||
options.update(await self.get_options_definitions(ha_id, program_key))
|
||||
|
||||
for option in options.values():
|
||||
option_value = option.constraints.default if option.constraints else None
|
||||
if option_value is not None:
|
||||
option_event_key = EventKey(option.key)
|
||||
events[option_event_key] = Event(
|
||||
option_event_key,
|
||||
option.key.value,
|
||||
0,
|
||||
"",
|
||||
"",
|
||||
option_value,
|
||||
option.name,
|
||||
unit=option.unit,
|
||||
)
|
||||
options_to_notify.update(options)
|
||||
for option_key in options_to_notify:
|
||||
for listener in self.context_listeners.get(
|
||||
(ha_id, EventKey(option_key)),
|
||||
[],
|
||||
):
|
||||
listener()
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
"""Home Connect entity base class."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, OptionKey
|
||||
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError
|
||||
from aiohomeconnect.model import EventKey
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectCoordinator
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -65,59 +60,3 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
return (
|
||||
self.appliance.info.connected and self._attr_available and super().available
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectOptionEntity(HomeConnectEntity):
|
||||
"""Class for entities that represents program options."""
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self.bsh_key in self.appliance.options
|
||||
|
||||
@property
|
||||
def option_value(self) -> str | int | float | bool | None:
|
||||
"""Return the state of the entity."""
|
||||
if event := self.appliance.events.get(EventKey(self.bsh_key)):
|
||||
return event.value
|
||||
return None
|
||||
|
||||
async def async_set_option(self, value: str | float | bool) -> None:
|
||||
"""Set an option for the entity."""
|
||||
try:
|
||||
# We try to set the active program option first,
|
||||
# if it fails we try to set the selected program option
|
||||
with contextlib.suppress(ActiveProgramNotSetError):
|
||||
await self.coordinator.client.set_active_program_option(
|
||||
self.appliance.info.ha_id,
|
||||
option_key=self.bsh_key,
|
||||
value=value,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Updated %s for the active program, new state: %s",
|
||||
self.entity_id,
|
||||
self.state,
|
||||
)
|
||||
return
|
||||
|
||||
await self.coordinator.client.set_selected_program_option(
|
||||
self.appliance.info.ha_id,
|
||||
option_key=self.bsh_key,
|
||||
value=value,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Updated %s for the selected program, new state: %s",
|
||||
self.entity_id,
|
||||
self.state,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_option",
|
||||
translation_placeholders=get_dict_from_home_connect_error(err),
|
||||
) from err
|
||||
|
||||
@property
|
||||
def bsh_key(self) -> OptionKey:
|
||||
"""Return the BSH key."""
|
||||
return cast(OptionKey, self.entity_description.key)
|
||||
|
||||
@@ -208,39 +208,6 @@
|
||||
},
|
||||
"door-assistant_freezer": {
|
||||
"default": "mdi:door"
|
||||
},
|
||||
"silence_on_demand": {
|
||||
"default": "mdi:volume-mute",
|
||||
"state": {
|
||||
"on": "mdi:volume-mute",
|
||||
"off": "mdi:volume-high"
|
||||
}
|
||||
},
|
||||
"half_load": {
|
||||
"default": "mdi:fraction-one-half"
|
||||
},
|
||||
"hygiene_plus": {
|
||||
"default": "mdi:silverware-clean"
|
||||
},
|
||||
"eco_dry": {
|
||||
"default": "mdi:sprout"
|
||||
},
|
||||
"fast_pre_heat": {
|
||||
"default": "mdi:fire"
|
||||
},
|
||||
"i_dos_1_active": {
|
||||
"default": "mdi:numeric-1-circle"
|
||||
},
|
||||
"i_dos_2_active": {
|
||||
"default": "mdi:numeric-2-circle"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"start_in_relative": {
|
||||
"default": "mdi:progress-clock"
|
||||
},
|
||||
"finish_in_relative": {
|
||||
"default": "mdi:progress-clock"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import GetSetting, OptionKey, SettingKey
|
||||
from aiohomeconnect.model import GetSetting, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
|
||||
from homeassistant.components.number import (
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature, UnitOfTime, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -25,17 +24,11 @@ from .const import (
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
)
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
UNIT_MAP = {
|
||||
"seconds": UnitOfTime.SECONDS,
|
||||
"ml": UnitOfVolume.MILLILITERS,
|
||||
"°C": UnitOfTemperature.CELSIUS,
|
||||
"°F": UnitOfTemperature.FAHRENHEIT,
|
||||
}
|
||||
|
||||
NUMBERS = (
|
||||
NumberEntityDescription(
|
||||
@@ -83,11 +76,6 @@ NUMBERS = (
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="wine_compartment_3_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=SettingKey.COOKING_HOOD_COLOR_TEMPERATURE_PERCENT,
|
||||
translation_key="color_temperature_percent",
|
||||
native_unit_of_measurement="%",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=SettingKey.LAUNDRY_CARE_WASHER_I_DOS_1_BASE_LEVEL,
|
||||
device_class=NumberDeviceClass.VOLUME,
|
||||
@@ -100,32 +88,6 @@ NUMBERS = (
|
||||
),
|
||||
)
|
||||
|
||||
NUMBER_OPTIONS = (
|
||||
NumberEntityDescription(
|
||||
key=OptionKey.BSH_COMMON_DURATION,
|
||||
translation_key="duration",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=OptionKey.BSH_COMMON_FINISH_IN_RELATIVE,
|
||||
translation_key="finish_in_relative",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=OptionKey.BSH_COMMON_START_IN_RELATIVE,
|
||||
translation_key="start_in_relative",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FILL_QUANTITY,
|
||||
translation_key="fill_quantity",
|
||||
device_class=NumberDeviceClass.VOLUME,
|
||||
native_step=1,
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE,
|
||||
translation_key="setpoint_temperature",
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
@@ -139,18 +101,6 @@ def _get_entities_for_appliance(
|
||||
]
|
||||
|
||||
|
||||
def _get_option_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> list[HomeConnectOptionEntity]:
|
||||
"""Get a list of currently available option entities."""
|
||||
return [
|
||||
HomeConnectOptionNumberEntity(entry.runtime_data, appliance, description)
|
||||
for description in NUMBER_OPTIONS
|
||||
if description.key in appliance.options
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
@@ -161,7 +111,6 @@ async def async_setup_entry(
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
_get_option_entities_for_appliance,
|
||||
)
|
||||
|
||||
|
||||
@@ -235,44 +184,3 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
or not hasattr(self, "_attr_native_step")
|
||||
):
|
||||
await self.async_fetch_constraints()
|
||||
|
||||
|
||||
class HomeConnectOptionNumberEntity(HomeConnectOptionEntity, NumberEntity):
|
||||
"""Number option class for Home Connect."""
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the native value of the entity."""
|
||||
await self.async_set_option(value)
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
self._attr_native_value = cast(float | None, self.option_value)
|
||||
option_definition = self.appliance.options.get(self.bsh_key)
|
||||
if option_definition:
|
||||
if option_definition.unit:
|
||||
candidate_unit = UNIT_MAP.get(
|
||||
option_definition.unit, option_definition.unit
|
||||
)
|
||||
if (
|
||||
not hasattr(self, "_attr_native_unit_of_measurement")
|
||||
or candidate_unit != self._attr_native_unit_of_measurement
|
||||
):
|
||||
self._attr_native_unit_of_measurement = candidate_unit
|
||||
self.__dict__.pop("unit_of_measurement", None)
|
||||
option_constraints = option_definition.constraints
|
||||
if option_constraints:
|
||||
if (
|
||||
not hasattr(self, "_attr_native_min_value")
|
||||
or self._attr_native_min_value != option_constraints.min
|
||||
) and option_constraints.min:
|
||||
self._attr_native_min_value = option_constraints.min
|
||||
if (
|
||||
not hasattr(self, "_attr_native_max_value")
|
||||
or self._attr_native_max_value != option_constraints.max
|
||||
) and option_constraints.max:
|
||||
self._attr_native_max_value = option_constraints.max
|
||||
if (
|
||||
not hasattr(self, "_attr_native_step")
|
||||
or self._attr_native_step != option_constraints.step_size
|
||||
) and option_constraints.step_size:
|
||||
self._attr_native_step = option_constraints.step_size
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Provides a select platform for Home Connect."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey
|
||||
from aiohomeconnect.model import EventKey, ProgramKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.program import Execution
|
||||
|
||||
@@ -18,60 +17,18 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .common import setup_home_connect_entry
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
AVAILABLE_MAPS_ENUM,
|
||||
BEAN_AMOUNT_OPTIONS,
|
||||
BEAN_CONTAINER_OPTIONS,
|
||||
CLEANING_MODE_OPTIONS,
|
||||
COFFEE_MILK_RATIO_OPTIONS,
|
||||
COFFEE_TEMPERATURE_OPTIONS,
|
||||
DOMAIN,
|
||||
DRYING_TARGET_OPTIONS,
|
||||
FLOW_RATE_OPTIONS,
|
||||
HOT_WATER_TEMPERATURE_OPTIONS,
|
||||
INTENSIVE_LEVEL_OPTIONS,
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP,
|
||||
SPIN_SPEED_OPTIONS,
|
||||
SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
TEMPERATURE_OPTIONS,
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP,
|
||||
VARIO_PERFECT_OPTIONS,
|
||||
VENTING_LEVEL_OPTIONS,
|
||||
WARMING_LEVEL_OPTIONS,
|
||||
)
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"Cooking.Hood.EnumType.ColorTemperature.custom",
|
||||
"Cooking.Hood.EnumType.ColorTemperature.warm",
|
||||
"Cooking.Hood.EnumType.ColorTemperature.warmToNeutral",
|
||||
"Cooking.Hood.EnumType.ColorTemperature.neutral",
|
||||
"Cooking.Hood.EnumType.ColorTemperature.neutralToCold",
|
||||
"Cooking.Hood.EnumType.ColorTemperature.cold",
|
||||
)
|
||||
}
|
||||
|
||||
AMBIENT_LIGHT_COLOR_TEMPERATURE_ENUM = {
|
||||
**{
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in ("BSH.Common.EnumType.AmbientLightColor.CustomColor",)
|
||||
},
|
||||
**{
|
||||
str(option): f"BSH.Common.EnumType.AmbientLightColor.Color{option}"
|
||||
for option in range(1, 100)
|
||||
},
|
||||
}
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -87,14 +44,6 @@ class HomeConnectProgramSelectEntityDescription(
|
||||
error_translation_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeConnectSelectEntityDescription(SelectEntityDescription):
|
||||
"""Entity Description class for settings and options that have enumeration values."""
|
||||
|
||||
translation_key_values: dict[str, str]
|
||||
values_translation_key: dict[str, str]
|
||||
|
||||
|
||||
PROGRAM_SELECT_ENTITY_DESCRIPTIONS = (
|
||||
HomeConnectProgramSelectEntityDescription(
|
||||
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
@@ -116,225 +65,20 @@ PROGRAM_SELECT_ENTITY_DESCRIPTIONS = (
|
||||
),
|
||||
)
|
||||
|
||||
SELECT_ENTITY_DESCRIPTIONS = (
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=SettingKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_CURRENT_MAP,
|
||||
translation_key="current_map",
|
||||
options=list(AVAILABLE_MAPS_ENUM),
|
||||
translation_key_values=AVAILABLE_MAPS_ENUM,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in AVAILABLE_MAPS_ENUM.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=SettingKey.COOKING_HOOD_COLOR_TEMPERATURE,
|
||||
translation_key="functional_light_color_temperature",
|
||||
options=list(FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM),
|
||||
translation_key_values=FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in FUNCTIONAL_LIGHT_COLOR_TEMPERATURE_ENUM.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=SettingKey.BSH_COMMON_AMBIENT_LIGHT_COLOR,
|
||||
translation_key="ambient_light_color",
|
||||
options=list(AMBIENT_LIGHT_COLOR_TEMPERATURE_ENUM),
|
||||
translation_key_values=AMBIENT_LIGHT_COLOR_TEMPERATURE_ENUM,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in AMBIENT_LIGHT_COLOR_TEMPERATURE_ENUM.items()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
PROGRAM_SELECT_OPTION_ENTITY_DESCRIPTIONS = (
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_REFERENCE_MAP_ID,
|
||||
translation_key="reference_map_id",
|
||||
options=list(AVAILABLE_MAPS_ENUM),
|
||||
translation_key_values=AVAILABLE_MAPS_ENUM,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in AVAILABLE_MAPS_ENUM.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_MODE,
|
||||
translation_key="cleaning_mode",
|
||||
options=list(CLEANING_MODE_OPTIONS),
|
||||
translation_key_values=CLEANING_MODE_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in CLEANING_MODE_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_AMOUNT,
|
||||
translation_key="bean_amount",
|
||||
options=list(BEAN_AMOUNT_OPTIONS),
|
||||
translation_key_values=BEAN_AMOUNT_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in BEAN_AMOUNT_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_TEMPERATURE,
|
||||
translation_key="coffee_temperature",
|
||||
options=list(COFFEE_TEMPERATURE_OPTIONS),
|
||||
translation_key_values=COFFEE_TEMPERATURE_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in COFFEE_TEMPERATURE_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_CONTAINER_SELECTION,
|
||||
translation_key="bean_container",
|
||||
options=list(BEAN_CONTAINER_OPTIONS),
|
||||
translation_key_values=BEAN_CONTAINER_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in BEAN_CONTAINER_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FLOW_RATE,
|
||||
translation_key="flow_rate",
|
||||
options=list(FLOW_RATE_OPTIONS),
|
||||
translation_key_values=FLOW_RATE_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in FLOW_RATE_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_MILK_RATIO,
|
||||
translation_key="coffee_milk_ratio",
|
||||
options=list(COFFEE_MILK_RATIO_OPTIONS),
|
||||
translation_key_values=COFFEE_MILK_RATIO_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in FLOW_RATE_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_HOT_WATER_TEMPERATURE,
|
||||
translation_key="hot_water_temperature",
|
||||
options=list(HOT_WATER_TEMPERATURE_OPTIONS),
|
||||
translation_key_values=HOT_WATER_TEMPERATURE_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in HOT_WATER_TEMPERATURE_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.LAUNDRY_CARE_DRYER_DRYING_TARGET,
|
||||
translation_key="drying_target",
|
||||
options=list(DRYING_TARGET_OPTIONS),
|
||||
translation_key_values=DRYING_TARGET_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in DRYING_TARGET_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.COOKING_COMMON_HOOD_VENTING_LEVEL,
|
||||
translation_key="venting_level",
|
||||
options=list(VENTING_LEVEL_OPTIONS),
|
||||
translation_key_values=VENTING_LEVEL_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in VENTING_LEVEL_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.COOKING_COMMON_HOOD_INTENSIVE_LEVEL,
|
||||
translation_key="intensive_level",
|
||||
options=list(INTENSIVE_LEVEL_OPTIONS),
|
||||
translation_key_values=INTENSIVE_LEVEL_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in INTENSIVE_LEVEL_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.COOKING_OVEN_WARMING_LEVEL,
|
||||
translation_key="warming_level",
|
||||
options=list(WARMING_LEVEL_OPTIONS),
|
||||
translation_key_values=WARMING_LEVEL_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in WARMING_LEVEL_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE,
|
||||
translation_key="washer_temperature",
|
||||
options=list(TEMPERATURE_OPTIONS),
|
||||
translation_key_values=TEMPERATURE_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in TEMPERATURE_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.LAUNDRY_CARE_WASHER_SPIN_SPEED,
|
||||
translation_key="spin_speed",
|
||||
options=list(SPIN_SPEED_OPTIONS),
|
||||
translation_key_values=SPIN_SPEED_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in SPIN_SPEED_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
HomeConnectSelectEntityDescription(
|
||||
key=OptionKey.LAUNDRY_CARE_COMMON_VARIO_PERFECT,
|
||||
translation_key="vario_perfect",
|
||||
options=list(VARIO_PERFECT_OPTIONS),
|
||||
translation_key_values=VARIO_PERFECT_OPTIONS,
|
||||
values_translation_key={
|
||||
value: translation_key
|
||||
for translation_key, value in VARIO_PERFECT_OPTIONS.items()
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> list[HomeConnectEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
*(
|
||||
[
|
||||
HomeConnectProgramSelectEntity(entry.runtime_data, appliance, desc)
|
||||
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
|
||||
]
|
||||
if appliance.info.type in APPLIANCES_WITH_PROGRAMS
|
||||
else []
|
||||
),
|
||||
*[
|
||||
HomeConnectSelectEntity(entry.runtime_data, appliance, desc)
|
||||
for desc in SELECT_ENTITY_DESCRIPTIONS
|
||||
if desc.key in appliance.settings
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
def _get_option_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> list[HomeConnectOptionEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
HomeConnectSelectOptionEntity(entry.runtime_data, appliance, desc)
|
||||
for desc in PROGRAM_SELECT_OPTION_ENTITY_DESCRIPTIONS
|
||||
if desc.key in appliance.options
|
||||
]
|
||||
return (
|
||||
[
|
||||
HomeConnectProgramSelectEntity(entry.runtime_data, appliance, desc)
|
||||
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
|
||||
]
|
||||
if appliance.info.type in APPLIANCES_WITH_PROGRAMS
|
||||
else []
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -347,7 +91,6 @@ async def async_setup_entry(
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
_get_option_entities_for_appliance,
|
||||
)
|
||||
|
||||
|
||||
@@ -405,122 +148,3 @@ class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program_key.value,
|
||||
},
|
||||
) from err
|
||||
|
||||
|
||||
class HomeConnectSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
"""Select setting class for Home Connect."""
|
||||
|
||||
entity_description: HomeConnectSelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
desc: HomeConnectSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
desc,
|
||||
)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select new option."""
|
||||
value = self.entity_description.translation_key_values[option]
|
||||
try:
|
||||
await self.coordinator.client.set_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=cast(SettingKey, self.bsh_key),
|
||||
value=value,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=SVE_TRANSLATION_KEY_SET_SETTING,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
SVE_TRANSLATION_PLACEHOLDER_ENTITY_ID: self.entity_id,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY: self.bsh_key,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE: value,
|
||||
},
|
||||
) from err
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
data = self.appliance.settings[cast(SettingKey, self.bsh_key)]
|
||||
self._attr_current_option = self.entity_description.values_translation_key.get(
|
||||
data.value
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
setting = self.appliance.settings.get(cast(SettingKey, self.bsh_key))
|
||||
if (
|
||||
not setting
|
||||
or not setting.constraints
|
||||
or not setting.constraints.allowed_values
|
||||
):
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
setting = await self.coordinator.client.get_setting(
|
||||
self.appliance.info.ha_id,
|
||||
setting_key=cast(SettingKey, self.bsh_key),
|
||||
)
|
||||
|
||||
if setting and setting.constraints and setting.constraints.allowed_values:
|
||||
self._attr_options = [
|
||||
self.entity_description.values_translation_key[option]
|
||||
for option in setting.constraints.allowed_values
|
||||
if option in self.entity_description.values_translation_key
|
||||
]
|
||||
|
||||
|
||||
class HomeConnectSelectOptionEntity(HomeConnectOptionEntity, SelectEntity):
|
||||
"""Select option class for Home Connect."""
|
||||
|
||||
entity_description: HomeConnectSelectEntityDescription
|
||||
_original_option_keys: set[str | None]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
desc: HomeConnectSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._original_option_keys = set(desc.values_translation_key.keys())
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
desc,
|
||||
)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select new option."""
|
||||
await self.async_set_option(
|
||||
self.entity_description.translation_key_values[option]
|
||||
)
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
self._attr_current_option = (
|
||||
self.entity_description.values_translation_key.get(
|
||||
cast(str, self.option_value), None
|
||||
)
|
||||
if self.option_value is not None
|
||||
else None
|
||||
)
|
||||
if (
|
||||
(option_definition := self.appliance.options.get(self.bsh_key))
|
||||
and (option_constraints := option_definition.constraints)
|
||||
and option_constraints.allowed_values
|
||||
and self._original_option_keys != set(option_constraints.allowed_values)
|
||||
):
|
||||
self._original_option_keys = set(option_constraints.allowed_values)
|
||||
self._attr_options = [
|
||||
self.entity_description.values_translation_key[option]
|
||||
for option in self._original_option_keys
|
||||
if option is not None
|
||||
]
|
||||
self.__dict__.pop("options", None)
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
@@ -56,6 +56,12 @@ BSH_PROGRAM_SENSORS = (
|
||||
"WasherDryer",
|
||||
),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=EventKey.BSH_COMMON_OPTION_DURATION,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
appliance_types=("Oven",),
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=EventKey.BSH_COMMON_OPTION_PROGRAM_PROGRESS,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
|
||||
@@ -98,9 +98,6 @@
|
||||
},
|
||||
"required_program_or_one_option_at_least": {
|
||||
"message": "A program or at least one of the possible options for a program should be specified"
|
||||
},
|
||||
"set_option": {
|
||||
"message": "Error setting the option for the program: {error}"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
@@ -815,23 +812,6 @@
|
||||
"name": "Wine compartment door"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"open_door": {
|
||||
"name": "Open door"
|
||||
},
|
||||
"partly_open_door": {
|
||||
"name": "Partly open door"
|
||||
},
|
||||
"pause_program": {
|
||||
"name": "Pause program"
|
||||
},
|
||||
"resume_program": {
|
||||
"name": "Resume program"
|
||||
},
|
||||
"stop_program": {
|
||||
"name": "Stop program"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"cooking_lighting": {
|
||||
"name": "Functional light"
|
||||
@@ -874,29 +854,11 @@
|
||||
"wine_compartment_3_setpoint_temperature": {
|
||||
"name": "Wine compartment 3 temperature"
|
||||
},
|
||||
"color_temperature_percent": {
|
||||
"name": "Functional light color temperature percent"
|
||||
},
|
||||
"washer_i_dos_1_base_level": {
|
||||
"name": "i-Dos 1 base level"
|
||||
},
|
||||
"washer_i_dos_2_base_level": {
|
||||
"name": "i-Dos 2 base level"
|
||||
},
|
||||
"duration": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::b_s_h_common_option_duration::name%]"
|
||||
},
|
||||
"start_in_relative": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::b_s_h_common_option_start_in_relative::name%]"
|
||||
},
|
||||
"finish_in_relative": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::b_s_h_common_option_finish_in_relative::name%]"
|
||||
},
|
||||
"fill_quantity": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_fill_quantity::name%]"
|
||||
},
|
||||
"setpoint_temperature": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_setpoint_temperature::name%]"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -1217,226 +1179,6 @@
|
||||
"laundry_care_washer_dryer_program_wash_and_dry_60": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wash_and_dry_60%]",
|
||||
"laundry_care_washer_dryer_program_wash_and_dry_90": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wash_and_dry_90%]"
|
||||
}
|
||||
},
|
||||
"current_map": {
|
||||
"name": "Current map",
|
||||
"state": {
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_temp_map%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map1%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map2%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map3%]"
|
||||
}
|
||||
},
|
||||
"functional_light_color_temperature": {
|
||||
"name": "Functional light color temperature",
|
||||
"state": {
|
||||
"cooking_hood_enum_type_color_temperature_custom": "Custom",
|
||||
"cooking_hood_enum_type_color_temperature_warm": "Warm",
|
||||
"cooking_hood_enum_type_color_temperature_warm_to_neutral": "Warm to Neutral",
|
||||
"cooking_hood_enum_type_color_temperature_neutral": "Neutral",
|
||||
"cooking_hood_enum_type_color_temperature_neutral_to_cold": "Neutral to Cold",
|
||||
"cooking_hood_enum_type_color_temperature_cold": "Cold"
|
||||
}
|
||||
},
|
||||
"ambient_light_color": {
|
||||
"name": "Ambient light color",
|
||||
"state": {
|
||||
"b_s_h_common_enum_type_ambient_light_color_custom_color": "Custom"
|
||||
}
|
||||
},
|
||||
"reference_map_id": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_cleaning_robot_option_reference_map_id::name%]",
|
||||
"state": {
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_temp_map%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map1%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map2%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map3%]"
|
||||
}
|
||||
},
|
||||
"cleaning_mode": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_cleaning_robot_option_cleaning_mode::name%]",
|
||||
"state": {
|
||||
"consumer_products_cleaning_robot_enum_type_cleaning_modes_silent": "[%key:component::home_connect::selector::cleaning_mode::options::consumer_products_cleaning_robot_enum_type_cleaning_modes_silent%]",
|
||||
"consumer_products_cleaning_robot_enum_type_cleaning_modes_standard": "[%key:component::home_connect::selector::cleaning_mode::options::consumer_products_cleaning_robot_enum_type_cleaning_modes_standard%]",
|
||||
"consumer_products_cleaning_robot_enum_type_cleaning_modes_power": "[%key:component::home_connect::selector::cleaning_mode::options::consumer_products_cleaning_robot_enum_type_cleaning_modes_power%]"
|
||||
}
|
||||
},
|
||||
"bean_amount": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_bean_amount::name%]",
|
||||
"state": {
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_very_mild": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_very_mild%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_mild": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_mild%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_mild_plus": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_mild_plus%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_normal": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_normal%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_normal_plus": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_normal_plus%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_strong": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_strong%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_strong_plus": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_strong_plus%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_very_strong": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_very_strong%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_very_strong_plus": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_very_strong_plus%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_extra_strong": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_extra_strong%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_double_shot": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_double_shot%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus_plus": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus_plus%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_triple_shot": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_triple_shot%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_triple_shot_plus": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_triple_shot_plus%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_amount_coffee_ground": "[%key:component::home_connect::selector::bean_amount::options::consumer_products_coffee_maker_enum_type_bean_amount_coffee_ground%]"
|
||||
}
|
||||
},
|
||||
"coffee_temperature": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_coffee_temperature::name%]",
|
||||
"state": {
|
||||
"consumer_products_coffee_maker_enum_type_coffee_temperature_88_c": "[%key:component::home_connect::selector::coffee_temperature::options::consumer_products_coffee_maker_enum_type_coffee_temperature_88_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_temperature_90_c": "[%key:component::home_connect::selector::coffee_temperature::options::consumer_products_coffee_maker_enum_type_coffee_temperature_90_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_temperature_92_c": "[%key:component::home_connect::selector::coffee_temperature::options::consumer_products_coffee_maker_enum_type_coffee_temperature_92_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_temperature_94_c": "[%key:component::home_connect::selector::coffee_temperature::options::consumer_products_coffee_maker_enum_type_coffee_temperature_94_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_temperature_95_c": "[%key:component::home_connect::selector::coffee_temperature::options::consumer_products_coffee_maker_enum_type_coffee_temperature_95_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_temperature_96_c": "[%key:component::home_connect::selector::coffee_temperature::options::consumer_products_coffee_maker_enum_type_coffee_temperature_96_c%]"
|
||||
}
|
||||
},
|
||||
"bean_container": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_bean_container::name%]",
|
||||
"state": {
|
||||
"consumer_products_coffee_maker_enum_type_bean_container_selection_right": "[%key:component::home_connect::selector::bean_container::options::consumer_products_coffee_maker_enum_type_bean_container_selection_right%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_container_selection_left": "[%key:component::home_connect::selector::bean_container::options::consumer_products_coffee_maker_enum_type_bean_container_selection_left%]"
|
||||
}
|
||||
},
|
||||
"flow_rate": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_flow_rate::name%]",
|
||||
"state": {
|
||||
"consumer_products_coffee_maker_enum_type_flow_rate_normal": "[%key:component::home_connect::selector::flow_rate::options::consumer_products_coffee_maker_enum_type_flow_rate_normal%]",
|
||||
"consumer_products_coffee_maker_enum_type_flow_rate_intense": "[%key:component::home_connect::selector::flow_rate::options::consumer_products_coffee_maker_enum_type_flow_rate_intense%]",
|
||||
"consumer_products_coffee_maker_enum_type_flow_rate_intense_plus": "[%key:component::home_connect::selector::flow_rate::options::consumer_products_coffee_maker_enum_type_flow_rate_intense_plus%]"
|
||||
}
|
||||
},
|
||||
"coffee_milk_ratio": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_coffee_milk_ratio::name%]",
|
||||
"state": {
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_10_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_10_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_20_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_20_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_25_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_25_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_30_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_30_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_40_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_40_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_50_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_50_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_55_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_55_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_60_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_60_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_65_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_65_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_67_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_67_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_70_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_70_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_75_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_75_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_80_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_80_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_85_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_85_percent%]",
|
||||
"consumer_products_coffee_maker_enum_type_coffee_milk_ratio_90_percent": "[%key:component::home_connect::selector::coffee_milk_ratio::options::consumer_products_coffee_maker_enum_type_coffee_milk_ratio_90_percent%]"
|
||||
}
|
||||
},
|
||||
"hot_water_temperature": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_hot_water_temperature::name%]",
|
||||
"state": {
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_white_tea": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_white_tea%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_green_tea": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_green_tea%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_black_tea": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_black_tea%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_55_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_55_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_60_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_60_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_65_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_65_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_70_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_70_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_75_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_75_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_80_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_80_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_85_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_85_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_90_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_90_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_95_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_95_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_97_c": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_97_c%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_122_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_122_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_131_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_131_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_140_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_140_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_149_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_149_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_158_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_158_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_167_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_167_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_176_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_176_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_185_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_185_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_194_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_194_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_203_f": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_203_f%]",
|
||||
"consumer_products_coffee_maker_enum_type_hot_water_temperature_max": "[%key:component::home_connect::selector::hot_water_temperature::options::consumer_products_coffee_maker_enum_type_hot_water_temperature_max%]"
|
||||
}
|
||||
},
|
||||
"drying_target": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_dryer_option_drying_target::name%]",
|
||||
"state": {
|
||||
"laundry_care_dryer_enum_type_drying_target_iron_dry": "[%key:component::home_connect::selector::drying_target::options::laundry_care_dryer_enum_type_drying_target_iron_dry%]",
|
||||
"laundry_care_dryer_enum_type_drying_target_gentle_dry": "[%key:component::home_connect::selector::drying_target::options::laundry_care_dryer_enum_type_drying_target_gentle_dry%]",
|
||||
"laundry_care_dryer_enum_type_drying_target_cupboard_dry": "[%key:component::home_connect::selector::drying_target::options::laundry_care_dryer_enum_type_drying_target_cupboard_dry%]",
|
||||
"laundry_care_dryer_enum_type_drying_target_cupboard_dry_plus": "[%key:component::home_connect::selector::drying_target::options::laundry_care_dryer_enum_type_drying_target_cupboard_dry_plus%]",
|
||||
"laundry_care_dryer_enum_type_drying_target_extra_dry": "[%key:component::home_connect::selector::drying_target::options::laundry_care_dryer_enum_type_drying_target_extra_dry%]"
|
||||
}
|
||||
},
|
||||
"venting_level": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_venting_level::name%]",
|
||||
"state": {
|
||||
"cooking_hood_enum_type_stage_fan_off": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_off%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage01%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage02": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage02%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage03": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage03%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage04": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage04%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage05": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage05%]"
|
||||
}
|
||||
},
|
||||
"intensive_level": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_intensive_level::name%]",
|
||||
"state": {
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage_off": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage_off%]",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage1": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage1%]",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage2": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage2%]"
|
||||
}
|
||||
},
|
||||
"warming_level": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_warming_level::name%]",
|
||||
"state": {
|
||||
"cooking_oven_enum_type_warming_level_low": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_low%]",
|
||||
"cooking_oven_enum_type_warming_level_medium": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_medium%]",
|
||||
"cooking_oven_enum_type_warming_level_high": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_high%]"
|
||||
}
|
||||
},
|
||||
"washer_temperature": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_temperature::name%]",
|
||||
"state": {
|
||||
"laundry_care_washer_enum_type_temperature_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_cold%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c20": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c20%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c30": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c30%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c40": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c40%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c50": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c50%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c60": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c60%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c70": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c70%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c80": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c80%]",
|
||||
"laundry_care_washer_enum_type_temperature_g_c90": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_g_c90%]",
|
||||
"laundry_care_washer_enum_type_temperature_ul_cold": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_cold%]",
|
||||
"laundry_care_washer_enum_type_temperature_ul_warm": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_warm%]",
|
||||
"laundry_care_washer_enum_type_temperature_ul_hot": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_hot%]",
|
||||
"laundry_care_washer_enum_type_temperature_ul_extra_hot": "[%key:component::home_connect::selector::washer_temperature::options::laundry_care_washer_enum_type_temperature_ul_extra_hot%]"
|
||||
}
|
||||
},
|
||||
"spin_speed": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]",
|
||||
"state": {
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m800": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m800%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1000": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1000%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1200%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m1600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_high%]"
|
||||
}
|
||||
},
|
||||
"vario_perfect": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_vario_perfect::name%]",
|
||||
"state": {
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_off%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_eco_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_eco_perfect%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_speed_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_speed_perfect%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -1623,45 +1365,6 @@
|
||||
},
|
||||
"door_assistant_freezer": {
|
||||
"name": "Freezer door assistant"
|
||||
},
|
||||
"multiple_beverages": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_multiple_beverages::name%]"
|
||||
},
|
||||
"intensiv_zone": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_intensiv_zone::name%]"
|
||||
},
|
||||
"brilliance_dry": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_brilliance_dry::name%]"
|
||||
},
|
||||
"vario_speed_plus": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_vario_speed_plus::name%]"
|
||||
},
|
||||
"silence_on_demand": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_silence_on_demand::name%]"
|
||||
},
|
||||
"half_load": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_half_load::name%]"
|
||||
},
|
||||
"extra_dry": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_extra_dry::name%]"
|
||||
},
|
||||
"hygiene_plus": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_hygiene_plus::name%]"
|
||||
},
|
||||
"eco_dry": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_eco_dry::name%]"
|
||||
},
|
||||
"zeolite_dry": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_zeolite_dry::name%]"
|
||||
},
|
||||
"fast_pre_heat": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_fast_pre_heat::name%]"
|
||||
},
|
||||
"i_dos1_active": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos1_active::name%]"
|
||||
},
|
||||
"i_dos2_active": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos2_active::name%]"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey
|
||||
from aiohomeconnect.model import EventKey, ProgramKey, SettingKey
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
from aiohomeconnect.model.program import EnumerateProgram
|
||||
|
||||
@@ -37,7 +37,7 @@ from .coordinator import (
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity, HomeConnectOptionEntity
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -100,61 +100,6 @@ POWER_SWITCH_DESCRIPTION = SwitchEntityDescription(
|
||||
translation_key="power",
|
||||
)
|
||||
|
||||
SWITCH_OPTIONS = (
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_MULTIPLE_BEVERAGES,
|
||||
translation_key="multiple_beverages",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_INTENSIV_ZONE,
|
||||
translation_key="intensiv_zone",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_BRILLIANCE_DRY,
|
||||
translation_key="brilliance_dry",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_VARIO_SPEED_PLUS,
|
||||
translation_key="vario_speed_plus",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_SILENCE_ON_DEMAND,
|
||||
translation_key="silence_on_demand",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_HALF_LOAD,
|
||||
translation_key="half_load",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_EXTRA_DRY,
|
||||
translation_key="extra_dry",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_HYGIENE_PLUS,
|
||||
translation_key="hygiene_plus",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_ECO_DRY,
|
||||
translation_key="eco_dry",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.DISHCARE_DISHWASHER_ZEOLITE_DRY,
|
||||
translation_key="zeolite_dry",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.COOKING_OVEN_FAST_PRE_HEAT,
|
||||
translation_key="fast_pre_heat",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE,
|
||||
translation_key="i_dos1_active",
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=OptionKey.LAUNDRY_CARE_WASHER_I_DOS_2_ACTIVE,
|
||||
translation_key="i_dos2_active",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
@@ -178,21 +123,10 @@ def _get_entities_for_appliance(
|
||||
for description in SWITCHES
|
||||
if description.key in appliance.settings
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
def _get_option_entities_for_appliance(
|
||||
entry: HomeConnectConfigEntry,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> list[HomeConnectOptionEntity]:
|
||||
"""Get a list of currently available option entities."""
|
||||
return [
|
||||
HomeConnectSwitchOptionEntity(entry.runtime_data, appliance, description)
|
||||
for description in SWITCH_OPTIONS
|
||||
if description.key in appliance.options
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
@@ -203,7 +137,6 @@ async def async_setup_entry(
|
||||
entry,
|
||||
_get_entities_for_appliance,
|
||||
async_add_entities,
|
||||
_get_option_entities_for_appliance,
|
||||
)
|
||||
|
||||
|
||||
@@ -470,19 +403,3 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
||||
self.power_off_state = BSH_POWER_STANDBY
|
||||
else:
|
||||
self.power_off_state = None
|
||||
|
||||
|
||||
class HomeConnectSwitchOptionEntity(HomeConnectOptionEntity, SwitchEntity):
|
||||
"""Switch option class for Home Connect."""
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the option."""
|
||||
await self.async_set_option(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the option."""
|
||||
await self.async_set_option(False)
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the value of the entity."""
|
||||
self._attr_is_on = cast(bool | None, self.option_value)
|
||||
|
||||
@@ -28,13 +28,12 @@ from . import silabs_multiprotocol_addon
|
||||
from .const import OTBR_DOMAIN, ZHA_DOMAIN
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
OwningAddon,
|
||||
OwningIntegration,
|
||||
get_otbr_addon_manager,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
guess_hardware_owners,
|
||||
probe_silabs_firmware_info,
|
||||
probe_silabs_firmware_type,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -53,7 +52,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Instantiate base flow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._probed_firmware_info: FirmwareInfo | None = None
|
||||
self._probed_firmware_type: ApplicationType | None = None
|
||||
self._device: str | None = None # To be set in a subclass
|
||||
self._hardware_name: str = "unknown" # To be set in a subclass
|
||||
|
||||
@@ -65,8 +64,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Shared translation placeholders."""
|
||||
placeholders = {
|
||||
"firmware_type": (
|
||||
self._probed_firmware_info.firmware_type.value
|
||||
if self._probed_firmware_info is not None
|
||||
self._probed_firmware_type.value
|
||||
if self._probed_firmware_type is not None
|
||||
else "unknown"
|
||||
),
|
||||
"model": self._hardware_name,
|
||||
@@ -121,49 +120,39 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
async def _probe_firmware_info(
|
||||
self,
|
||||
probe_methods: tuple[ApplicationType, ...] = (
|
||||
# We probe in order of frequency: Zigbee, Thread, then multi-PAN
|
||||
ApplicationType.GECKO_BOOTLOADER,
|
||||
ApplicationType.EZSP,
|
||||
ApplicationType.SPINEL,
|
||||
ApplicationType.CPC,
|
||||
),
|
||||
) -> bool:
|
||||
async def _probe_firmware_type(self) -> bool:
|
||||
"""Probe the firmware currently on the device."""
|
||||
assert self._device is not None
|
||||
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._probed_firmware_type = await probe_silabs_firmware_type(
|
||||
self._device,
|
||||
probe_methods=probe_methods,
|
||||
)
|
||||
|
||||
return (
|
||||
self._probed_firmware_info is not None
|
||||
and self._probed_firmware_info.firmware_type
|
||||
in (
|
||||
probe_methods=(
|
||||
# We probe in order of frequency: Zigbee, Thread, then multi-PAN
|
||||
ApplicationType.GECKO_BOOTLOADER,
|
||||
ApplicationType.EZSP,
|
||||
ApplicationType.SPINEL,
|
||||
ApplicationType.CPC,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
return self._probed_firmware_type in (
|
||||
ApplicationType.EZSP,
|
||||
ApplicationType.SPINEL,
|
||||
ApplicationType.CPC,
|
||||
)
|
||||
|
||||
async def async_step_pick_firmware_zigbee(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick Zigbee firmware."""
|
||||
if not await self._probe_firmware_info():
|
||||
if not await self._probe_firmware_type():
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
|
||||
# Allow the stick to be used with ZHA without flashing
|
||||
if (
|
||||
self._probed_firmware_info is not None
|
||||
and self._probed_firmware_info.firmware_type == ApplicationType.EZSP
|
||||
):
|
||||
if self._probed_firmware_type == ApplicationType.EZSP:
|
||||
return await self.async_step_confirm_zigbee()
|
||||
|
||||
if not is_hassio(self.hass):
|
||||
@@ -349,12 +338,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Confirm Zigbee setup."""
|
||||
assert self._device is not None
|
||||
assert self._hardware_name is not None
|
||||
|
||||
if not await self._probe_firmware_info(probe_methods=(ApplicationType.EZSP,)):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
self._probed_firmware_type = ApplicationType.EZSP
|
||||
|
||||
if user_input is not None:
|
||||
await self.hass.config_entries.flow.async_init(
|
||||
@@ -382,7 +366,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick Thread firmware."""
|
||||
if not await self._probe_firmware_info():
|
||||
if not await self._probe_firmware_type():
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
@@ -474,11 +458,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Confirm OTBR setup."""
|
||||
assert self._device is not None
|
||||
|
||||
if not await self._probe_firmware_info(probe_methods=(ApplicationType.SPINEL,)):
|
||||
return self.async_abort(
|
||||
reason="unsupported_firmware",
|
||||
description_placeholders=self._get_translation_placeholders(),
|
||||
)
|
||||
self._probed_firmware_type = ApplicationType.SPINEL
|
||||
|
||||
if user_input is not None:
|
||||
# OTBR discovery is done automatically via hassio
|
||||
@@ -517,14 +497,14 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
|
||||
class BaseFirmwareOptionsFlow(BaseFirmwareInstallFlow, OptionsFlow):
|
||||
"""Zigbee and Thread options flow handlers."""
|
||||
|
||||
_probed_firmware_info: FirmwareInfo
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry, *args: Any, **kwargs: Any) -> None:
|
||||
"""Instantiate options flow."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._config_entry = config_entry
|
||||
|
||||
self._probed_firmware_type = ApplicationType(self.config_entry.data["firmware"])
|
||||
|
||||
# Make `context` a regular dictionary
|
||||
self.context = {}
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": ["universal-silabs-flasher==0.0.29"]
|
||||
"requirements": ["universal-silabs-flasher==0.0.25"]
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ class ApplicationType(StrEnum):
|
||||
CPC = "cpc"
|
||||
EZSP = "ezsp"
|
||||
SPINEL = "spinel"
|
||||
ROUTER = "router"
|
||||
|
||||
@classmethod
|
||||
def from_flasher_application_type(
|
||||
@@ -249,10 +248,10 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware
|
||||
return guesses[-1][0]
|
||||
|
||||
|
||||
async def probe_silabs_firmware_info(
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
|
||||
) -> FirmwareInfo | None:
|
||||
"""Probe the running firmware on a SiLabs device."""
|
||||
) -> ApplicationType | None:
|
||||
"""Probe the running firmware on a Silabs device."""
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
**(
|
||||
@@ -270,26 +269,4 @@ async def probe_silabs_firmware_info(
|
||||
if flasher.app_type is None:
|
||||
return None
|
||||
|
||||
return FirmwareInfo(
|
||||
device=device,
|
||||
firmware_type=ApplicationType.from_flasher_application_type(flasher.app_type),
|
||||
firmware_version=(
|
||||
flasher.app_version.orig_version
|
||||
if flasher.app_version is not None
|
||||
else None
|
||||
),
|
||||
source="probe",
|
||||
owners=[],
|
||||
)
|
||||
|
||||
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str, *, probe_methods: Iterable[ApplicationType] | None = None
|
||||
) -> ApplicationType | None:
|
||||
"""Probe the running firmware type on a SiLabs device."""
|
||||
|
||||
fw_info = await probe_silabs_firmware_info(device, probe_methods=probe_methods)
|
||||
if fw_info is None:
|
||||
return None
|
||||
|
||||
return fw_info.firmware_type
|
||||
return ApplicationType.from_flasher_application_type(flasher.app_type)
|
||||
|
||||
@@ -10,10 +10,7 @@ from homeassistant.components.homeassistant_hardware import (
|
||||
firmware_config_flow,
|
||||
silabs_multiprotocol_addon,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryBaseFlow,
|
||||
@@ -121,7 +118,7 @@ class HomeAssistantSkyConnectConfigFlow(
|
||||
"""Create the config entry."""
|
||||
assert self._usb_info is not None
|
||||
assert self._hw_variant is not None
|
||||
assert self._probed_firmware_info is not None
|
||||
assert self._probed_firmware_type is not None
|
||||
|
||||
return self.async_create_entry(
|
||||
title=self._hw_variant.full_name,
|
||||
@@ -133,7 +130,7 @@ class HomeAssistantSkyConnectConfigFlow(
|
||||
"description": self._usb_info.description, # For backwards compatibility
|
||||
"product": self._usb_info.description,
|
||||
"device": self._usb_info.device,
|
||||
"firmware": self._probed_firmware_info.firmware_type.value,
|
||||
"firmware": self._probed_firmware_type.value,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -206,26 +203,18 @@ class HomeAssistantSkyConnectOptionsFlowHandler(
|
||||
self._hardware_name = self._hw_variant.full_name
|
||||
self._device = self._usb_info.device
|
||||
|
||||
self._probed_firmware_info = FirmwareInfo(
|
||||
device=self._device,
|
||||
firmware_type=ApplicationType(self.config_entry.data["firmware"]),
|
||||
firmware_version=None,
|
||||
source="guess",
|
||||
owners=[],
|
||||
)
|
||||
|
||||
# Regenerate the translation placeholders
|
||||
self._get_translation_placeholders()
|
||||
|
||||
def _async_flow_finished(self) -> ConfigFlowResult:
|
||||
"""Create the config entry."""
|
||||
assert self._probed_firmware_info is not None
|
||||
assert self._probed_firmware_type is not None
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry=self.config_entry,
|
||||
data={
|
||||
**self.config_entry.data,
|
||||
"firmware": self._probed_firmware_info.firmware_type.value,
|
||||
"firmware": self._probed_firmware_type.value,
|
||||
},
|
||||
options=self.config_entry.options,
|
||||
)
|
||||
|
||||
@@ -24,10 +24,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
|
||||
OptionsFlowHandler as MultiprotocolOptionsFlowHandler,
|
||||
SerialPortSettings as MultiprotocolSerialPortSettings,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
)
|
||||
from homeassistant.components.homeassistant_hardware.util import ApplicationType
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_HARDWARE,
|
||||
ConfigEntry,
|
||||
@@ -82,13 +79,10 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
|
||||
await self._probe_firmware_info()
|
||||
await self._probe_firmware_type()
|
||||
|
||||
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
|
||||
if (
|
||||
self._probed_firmware_info is not None
|
||||
and self._probed_firmware_info.firmware_type is ApplicationType.EZSP
|
||||
):
|
||||
if self._probed_firmware_type is ApplicationType.EZSP:
|
||||
discovery_flow.async_create_flow(
|
||||
self.hass,
|
||||
ZHA_DOMAIN,
|
||||
@@ -104,11 +98,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
title=BOARD_NAME,
|
||||
data={
|
||||
# Assume the firmware type is EZSP if we cannot probe it
|
||||
FIRMWARE: (
|
||||
self._probed_firmware_info.firmware_type
|
||||
if self._probed_firmware_info is not None
|
||||
else ApplicationType.EZSP
|
||||
).value,
|
||||
FIRMWARE: (self._probed_firmware_type or ApplicationType.EZSP).value,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -274,14 +264,6 @@ class HomeAssistantYellowOptionsFlowHandler(
|
||||
self._hardware_name = BOARD_NAME
|
||||
self._device = RADIO_DEVICE
|
||||
|
||||
self._probed_firmware_info = FirmwareInfo(
|
||||
device=self._device,
|
||||
firmware_type=ApplicationType(self.config_entry.data["firmware"]),
|
||||
firmware_version=None,
|
||||
source="guess",
|
||||
owners=[],
|
||||
)
|
||||
|
||||
# Regenerate the translation placeholders
|
||||
self._get_translation_placeholders()
|
||||
|
||||
@@ -303,13 +285,13 @@ class HomeAssistantYellowOptionsFlowHandler(
|
||||
|
||||
def _async_flow_finished(self) -> ConfigFlowResult:
|
||||
"""Create the config entry."""
|
||||
assert self._probed_firmware_info is not None
|
||||
assert self._probed_firmware_type is not None
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
entry=self.config_entry,
|
||||
data={
|
||||
**self.config_entry.data,
|
||||
FIRMWARE: self._probed_firmware_info.firmware_type.value,
|
||||
FIRMWARE: self._probed_firmware_type.value,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
|
||||
AttributeType.RAIN_FALL_TODAY: HomeeSensorEntityDescription(
|
||||
key="rainfall_day",
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
AttributeType.RELATIVE_HUMIDITY: HomeeSensorEntityDescription(
|
||||
key="humidity",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"loggers": ["pyhap"],
|
||||
"requirements": [
|
||||
"HAP-python==4.9.2",
|
||||
"fnv-hash-fast==1.2.3",
|
||||
"fnv-hash-fast==1.2.2",
|
||||
"PyQRCode==1.2.1",
|
||||
"base36==0.1.1"
|
||||
],
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohue"],
|
||||
"requirements": ["aiohue==4.7.4"],
|
||||
"requirements": ["aiohue==4.7.3"],
|
||||
"zeroconf": ["_hue._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -107,9 +107,7 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
self._attr_effect_list = []
|
||||
if effects := resource.effects:
|
||||
self._attr_effect_list = [
|
||||
x.value
|
||||
for x in effects.status_values
|
||||
if x not in (EffectStatus.NO_EFFECT, EffectStatus.UNKNOWN)
|
||||
x.value for x in effects.status_values if x != EffectStatus.NO_EFFECT
|
||||
]
|
||||
if timed_effects := resource.timed_effects:
|
||||
self._attr_effect_list += [
|
||||
|
||||
@@ -4,20 +4,17 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
|
||||
from inkbird_ble import INKBIRDBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfo,
|
||||
)
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_DEVICE_TYPE, DOMAIN
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
@@ -28,33 +25,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up INKBIRD BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
device_type: str | None = entry.data.get(CONF_DEVICE_TYPE)
|
||||
data = INKBIRDBluetoothDeviceData(device_type)
|
||||
|
||||
@callback
|
||||
def _async_on_update(service_info: BluetoothServiceInfo) -> SensorUpdate:
|
||||
"""Handle update callback from the passive BLE processor."""
|
||||
nonlocal device_type
|
||||
update = data.update(service_info)
|
||||
if device_type is None and data.device_type is not None:
|
||||
device_type_str = str(data.device_type)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_DEVICE_TYPE: device_type_str}
|
||||
)
|
||||
device_type = device_type_str
|
||||
return update
|
||||
|
||||
coordinator = PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=_async_on_update,
|
||||
data = INKBIRDBluetoothDeviceData()
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
PassiveBluetoothProcessorCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=data.update,
|
||||
)
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
# only start after all platforms have had a chance to subscribe
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
) # only start after all platforms have had a chance to subscribe
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"""Constants for the INKBIRD Bluetooth integration."""
|
||||
|
||||
DOMAIN = "inkbird"
|
||||
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==0.7.0"]
|
||||
"requirements": ["inkbird-ble==0.5.8"]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.util.dt import parse_datetime
|
||||
|
||||
from .browse_media import build_item_response, build_root_response
|
||||
from .client_wrapper import get_artwork_url
|
||||
from .const import CONTENT_TYPE_MAP, LOGGER, MAX_IMAGE_WIDTH
|
||||
from .const import CONTENT_TYPE_MAP, LOGGER
|
||||
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
|
||||
from .entity import JellyfinClientEntity
|
||||
|
||||
@@ -169,9 +169,7 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
|
||||
if self.now_playing is None:
|
||||
return None
|
||||
|
||||
return get_artwork_url(
|
||||
self.coordinator.api_client, self.now_playing, MAX_IMAGE_WIDTH
|
||||
)
|
||||
return get_artwork_url(self.coordinator.api_client, self.now_playing, 150)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
|
||||
@@ -49,7 +49,6 @@ from .helpers import (
|
||||
InputType,
|
||||
async_update_config_entry,
|
||||
generate_unique_id,
|
||||
purge_device_registry,
|
||||
register_lcn_address_devices,
|
||||
register_lcn_host_device,
|
||||
)
|
||||
@@ -121,9 +120,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
register_lcn_host_device(hass, config_entry)
|
||||
register_lcn_address_devices(hass, config_entry)
|
||||
|
||||
# clean up orphaned devices
|
||||
purge_device_registry(hass, config_entry.entry_id, {**config_entry.data})
|
||||
|
||||
# forward config_entry to components
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
from collections.abc import Callable
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_RESOURCE
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_NAME, CONF_RESOURCE
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_DOMAIN_DATA, DOMAIN
|
||||
from .helpers import (
|
||||
AddressType,
|
||||
DeviceConnectionType,
|
||||
InputType,
|
||||
generate_unique_id,
|
||||
get_device_connection,
|
||||
get_device_model,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,14 +36,6 @@ class LcnEntity(Entity):
|
||||
self.address: AddressType = config[CONF_ADDRESS]
|
||||
self._unregister_for_inputs: Callable | None = None
|
||||
self._name: str = config[CONF_NAME]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
generate_unique_id(self.config_entry.entry_id, self.address),
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
@@ -51,6 +44,28 @@ class LcnEntity(Entity):
|
||||
self.config_entry.entry_id, self.address, self.config[CONF_RESOURCE]
|
||||
)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo | None:
|
||||
"""Return device specific attributes."""
|
||||
address = f"{'g' if self.address[2] else 'm'}{self.address[0]:03d}{self.address[1]:03d}"
|
||||
model = (
|
||||
"LCN resource"
|
||||
f" ({get_device_model(self.config[CONF_DOMAIN], self.config[CONF_DOMAIN_DATA])})"
|
||||
)
|
||||
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
name=f"{address}.{self.config[CONF_RESOURCE]}",
|
||||
model=model,
|
||||
manufacturer="Issendorff",
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
generate_unique_id(
|
||||
self.config_entry.entry_id, self.config[CONF_ADDRESS]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
self.device_connection = get_device_connection(
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from copy import deepcopy
|
||||
from itertools import chain
|
||||
import re
|
||||
from typing import cast
|
||||
|
||||
@@ -21,6 +22,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_RESOURCE,
|
||||
CONF_SENSORS,
|
||||
CONF_SOURCE,
|
||||
CONF_SWITCHES,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -28,14 +30,23 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
BINSENSOR_PORTS,
|
||||
CONF_CLIMATES,
|
||||
CONF_HARDWARE_SERIAL,
|
||||
CONF_HARDWARE_TYPE,
|
||||
CONF_OUTPUT,
|
||||
CONF_SCENES,
|
||||
CONF_SOFTWARE_SERIAL,
|
||||
CONNECTION,
|
||||
DEVICE_CONNECTIONS,
|
||||
DOMAIN,
|
||||
LED_PORTS,
|
||||
LOGICOP_PORTS,
|
||||
OUTPUT_PORTS,
|
||||
S0_INPUTS,
|
||||
SETPOINTS,
|
||||
THRESHOLDS,
|
||||
VARIABLES,
|
||||
)
|
||||
|
||||
# typing
|
||||
@@ -85,6 +96,31 @@ def get_resource(domain_name: str, domain_data: ConfigType) -> str:
|
||||
raise ValueError("Unknown domain")
|
||||
|
||||
|
||||
def get_device_model(domain_name: str, domain_data: ConfigType) -> str:
|
||||
"""Return the model for the specified domain_data."""
|
||||
if domain_name in ("switch", "light"):
|
||||
return "Output" if domain_data[CONF_OUTPUT] in OUTPUT_PORTS else "Relay"
|
||||
if domain_name in ("binary_sensor", "sensor"):
|
||||
if domain_data[CONF_SOURCE] in BINSENSOR_PORTS:
|
||||
return "Binary Sensor"
|
||||
if domain_data[CONF_SOURCE] in chain(
|
||||
VARIABLES, SETPOINTS, THRESHOLDS, S0_INPUTS
|
||||
):
|
||||
return "Variable"
|
||||
if domain_data[CONF_SOURCE] in LED_PORTS:
|
||||
return "Led"
|
||||
if domain_data[CONF_SOURCE] in LOGICOP_PORTS:
|
||||
return "Logical Operation"
|
||||
return "Key"
|
||||
if domain_name == "cover":
|
||||
return "Motor"
|
||||
if domain_name == "climate":
|
||||
return "Regulator"
|
||||
if domain_name == "scene":
|
||||
return "Scene"
|
||||
raise ValueError("Unknown domain")
|
||||
|
||||
|
||||
def generate_unique_id(
|
||||
entry_id: str,
|
||||
address: AddressType,
|
||||
@@ -133,6 +169,13 @@ def purge_device_registry(
|
||||
) -> None:
|
||||
"""Remove orphans from device registry which are not in entry data."""
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Find all devices that are referenced in the entity registry.
|
||||
references_entities = {
|
||||
entry.device_id
|
||||
for entry in entity_registry.entities.get_entries_for_config_entry_id(entry_id)
|
||||
}
|
||||
|
||||
# Find device that references the host.
|
||||
references_host = set()
|
||||
@@ -155,6 +198,7 @@ def purge_device_registry(
|
||||
entry.id
|
||||
for entry in dr.async_entries_for_config_entry(device_registry, entry_id)
|
||||
}
|
||||
- references_entities
|
||||
- references_host
|
||||
- references_entry_data
|
||||
)
|
||||
|
||||
@@ -581,44 +581,36 @@ class ThinQSensorEntity(ThinQEntity, SensorEntity):
|
||||
local_now = datetime.now(
|
||||
tz=dt_util.get_time_zone(self.coordinator.hass.config.time_zone)
|
||||
)
|
||||
self._device_state = (
|
||||
self.coordinator.data[self._device_state_id].value
|
||||
if self._device_state_id in self.coordinator.data
|
||||
else None
|
||||
)
|
||||
if value in [0, None, time.min] or (
|
||||
self._device_state == "power_off"
|
||||
and self.entity_description.key
|
||||
in [TimerProperty.REMAIN, TimerProperty.TOTAL]
|
||||
):
|
||||
# Reset to None when power_off
|
||||
if value in [0, None, time.min]:
|
||||
# Reset to None
|
||||
value = None
|
||||
elif self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
|
||||
if self.entity_description.key in TIME_SENSOR_DESC:
|
||||
# Set timestamp for absolute time
|
||||
# Set timestamp for time
|
||||
value = local_now.replace(hour=value.hour, minute=value.minute)
|
||||
else:
|
||||
# Set timestamp for delta
|
||||
event_data = timedelta(
|
||||
new_state = (
|
||||
self.coordinator.data[self._device_state_id].value
|
||||
if self._device_state_id in self.coordinator.data
|
||||
else None
|
||||
)
|
||||
if (
|
||||
self.native_value is not None
|
||||
and self._device_state == new_state
|
||||
):
|
||||
# Skip update when same state
|
||||
return
|
||||
|
||||
self._device_state = new_state
|
||||
time_delta = timedelta(
|
||||
hours=value.hour, minutes=value.minute, seconds=value.second
|
||||
)
|
||||
new_time = (
|
||||
(local_now - event_data)
|
||||
value = (
|
||||
(local_now - time_delta)
|
||||
if self.entity_description.key == TimerProperty.RUNNING
|
||||
else (local_now + event_data)
|
||||
else (local_now + time_delta)
|
||||
)
|
||||
# The remain_time may change during the wash/dry operation depending on various reasons.
|
||||
# If there is a diff of more than 60sec, the new timestamp is used
|
||||
if (
|
||||
parse_native_value := dt_util.parse_datetime(
|
||||
str(self.native_value)
|
||||
)
|
||||
) is None or abs(new_time - parse_native_value) > timedelta(
|
||||
seconds=60
|
||||
):
|
||||
value = new_time
|
||||
else:
|
||||
value = self.native_value
|
||||
elif self.entity_description.device_class == SensorDeviceClass.DURATION:
|
||||
# Set duration
|
||||
value = self._get_duration(
|
||||
|
||||
@@ -9,13 +9,15 @@ import dns.rdata
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_PORT, Platform
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_HOST, CONF_PORT, CONF_TYPE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .api import MinecraftServer, MinecraftServerAddressError, MinecraftServerType
|
||||
from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
|
||||
from .coordinator import MinecraftServerConfigEntry, MinecraftServerCoordinator
|
||||
from .coordinator import MinecraftServerCoordinator
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
@@ -29,18 +31,32 @@ def load_dnspython_rdata_classes() -> None:
|
||||
dns.rdata.get_rdata_class(dns.rdataclass.IN, rdtype) # type: ignore[no-untyped-call]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MinecraftServerConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Minecraft Server from a config entry."""
|
||||
|
||||
# Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
|
||||
await hass.async_add_executor_job(load_dnspython_rdata_classes)
|
||||
|
||||
# Create coordinator instance and store it.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry)
|
||||
# Create API instance.
|
||||
api = MinecraftServer(
|
||||
hass,
|
||||
entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION),
|
||||
entry.data[CONF_ADDRESS],
|
||||
)
|
||||
|
||||
# Initialize API instance.
|
||||
try:
|
||||
await api.async_initialize()
|
||||
except MinecraftServerAddressError as error:
|
||||
raise ConfigEntryNotReady(f"Initialization failed: {error}") from error
|
||||
|
||||
# Create coordinator instance.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry, api)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
# Store coordinator instance.
|
||||
domain_data = hass.data.setdefault(DOMAIN, {})
|
||||
domain_data[entry.entry_id] = coordinator
|
||||
|
||||
# Set up platforms.
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
@@ -48,16 +64,21 @@ async def async_setup_entry(
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: MinecraftServerConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload Minecraft Server config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
# Unload platforms.
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
|
||||
# Clean up.
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: MinecraftServerConfigEntry
|
||||
) -> bool:
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old config entry to a new format."""
|
||||
|
||||
# 1 --> 2: Use config entry ID as base for unique IDs.
|
||||
@@ -131,9 +152,7 @@ async def async_migrate_entry(
|
||||
|
||||
|
||||
async def _async_migrate_device_identifiers(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MinecraftServerConfigEntry,
|
||||
old_unique_id: str | None,
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, old_unique_id: str | None
|
||||
) -> None:
|
||||
"""Migrate the device identifiers to the new format."""
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -5,10 +5,12 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import MinecraftServerConfigEntry, MinecraftServerCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MinecraftServerCoordinator
|
||||
from .entity import MinecraftServerEntity
|
||||
|
||||
KEY_STATUS = "status"
|
||||
@@ -25,11 +27,11 @@ BINARY_SENSOR_DESCRIPTIONS = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MinecraftServerConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Minecraft Server binary sensor platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
# Add binary sensor entities.
|
||||
async_add_entities(
|
||||
@@ -47,7 +49,7 @@ class MinecraftServerBinarySensorEntity(MinecraftServerEntity, BinarySensorEntit
|
||||
self,
|
||||
coordinator: MinecraftServerCoordinator,
|
||||
description: BinarySensorEntityDescription,
|
||||
config_entry: MinecraftServerConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize binary sensor base entity."""
|
||||
super().__init__(coordinator, config_entry)
|
||||
|
||||
@@ -6,22 +6,17 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .api import (
|
||||
MinecraftServer,
|
||||
MinecraftServerAddressError,
|
||||
MinecraftServerConnectionError,
|
||||
MinecraftServerData,
|
||||
MinecraftServerNotInitializedError,
|
||||
MinecraftServerType,
|
||||
)
|
||||
|
||||
type MinecraftServerConfigEntry = ConfigEntry[MinecraftServerCoordinator]
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -30,15 +25,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class MinecraftServerCoordinator(DataUpdateCoordinator[MinecraftServerData]):
|
||||
"""Minecraft Server data update coordinator."""
|
||||
|
||||
config_entry: MinecraftServerConfigEntry
|
||||
_api: MinecraftServer
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MinecraftServerConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
api: MinecraftServer,
|
||||
) -> None:
|
||||
"""Initialize coordinator instance."""
|
||||
self._api = api
|
||||
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
@@ -48,22 +44,6 @@ class MinecraftServerCoordinator(DataUpdateCoordinator[MinecraftServerData]):
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the Minecraft Server data coordinator."""
|
||||
|
||||
# Create API instance.
|
||||
self._api = MinecraftServer(
|
||||
self.hass,
|
||||
self.config_entry.data.get(CONF_TYPE, MinecraftServerType.JAVA_EDITION),
|
||||
self.config_entry.data[CONF_ADDRESS],
|
||||
)
|
||||
|
||||
# Initialize API instance.
|
||||
try:
|
||||
await self._api.async_initialize()
|
||||
except MinecraftServerAddressError as error:
|
||||
raise ConfigEntryNotReady(f"Initialization failed: {error}") from error
|
||||
|
||||
async def _async_update_data(self) -> MinecraftServerData:
|
||||
"""Get updated data from the server."""
|
||||
try:
|
||||
|
||||
@@ -5,19 +5,20 @@ from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import MinecraftServerConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
TO_REDACT: Iterable[Any] = {CONF_ADDRESS, CONF_NAME, "players_list"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: MinecraftServerConfigEntry
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
return {
|
||||
"config_entry": {
|
||||
|
||||
@@ -29,7 +29,7 @@ rules:
|
||||
status: done
|
||||
comment: Using confid entry ID as the dependency mcstatus doesn't provide a unique information.
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
runtime-data: todo
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: done
|
||||
|
||||
@@ -7,14 +7,15 @@ from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TYPE, EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .api import MinecraftServerData, MinecraftServerType
|
||||
from .const import KEY_LATENCY, KEY_MOTD
|
||||
from .coordinator import MinecraftServerConfigEntry, MinecraftServerCoordinator
|
||||
from .const import DOMAIN, KEY_LATENCY, KEY_MOTD
|
||||
from .coordinator import MinecraftServerCoordinator
|
||||
from .entity import MinecraftServerEntity
|
||||
|
||||
ATTR_PLAYERS_LIST = "players_list"
|
||||
@@ -157,11 +158,11 @@ SENSOR_DESCRIPTIONS = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MinecraftServerConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Minecraft Server sensor platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
# Add sensor entities.
|
||||
async_add_entities(
|
||||
@@ -183,7 +184,7 @@ class MinecraftServerSensorEntity(MinecraftServerEntity, SensorEntity):
|
||||
self,
|
||||
coordinator: MinecraftServerCoordinator,
|
||||
description: MinecraftServerSensorEntityDescription,
|
||||
config_entry: MinecraftServerConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize sensor base entity."""
|
||||
super().__init__(coordinator, config_entry)
|
||||
|
||||
@@ -384,11 +384,6 @@ class ModbusHub:
|
||||
{ATTR_SLAVE: slave} if slave is not None else {ATTR_SLAVE: 1}
|
||||
)
|
||||
entry = self._pb_request[use_call]
|
||||
|
||||
if use_call in {"write_registers", "write_coils"}:
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
|
||||
kwargs[entry.value_attr_name] = value
|
||||
try:
|
||||
result: ModbusPDU = await entry.func(address, **kwargs)
|
||||
|
||||
@@ -58,12 +58,12 @@
|
||||
},
|
||||
"services": {
|
||||
"lock_n_go": {
|
||||
"name": "Lock 'n' Go",
|
||||
"description": "Unlocks the door, waits a few seconds then re-locks. The wait period can be customized through the app.",
|
||||
"name": "Lock 'n' go",
|
||||
"description": "Nuki Lock 'n' Go.",
|
||||
"fields": {
|
||||
"unlatch": {
|
||||
"name": "Unlatch",
|
||||
"description": "Whether to also unlatch the door when unlocking it."
|
||||
"description": "Whether to unlatch the lock."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from html import unescape
|
||||
from json import dumps, loads
|
||||
import logging
|
||||
@@ -11,10 +10,10 @@ from typing import cast
|
||||
from onedrive_personal_sdk import OneDriveClient
|
||||
from onedrive_personal_sdk.exceptions import (
|
||||
AuthenticationError,
|
||||
NotFoundError,
|
||||
HttpRequestException,
|
||||
OneDriveException,
|
||||
)
|
||||
from onedrive_personal_sdk.models.items import Item, ItemUpdate
|
||||
from onedrive_personal_sdk.models.items import ItemUpdate
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -26,7 +25,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
|
||||
|
||||
from .const import CONF_FOLDER_ID, CONF_FOLDER_NAME, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .coordinator import (
|
||||
OneDriveConfigEntry,
|
||||
OneDriveRuntimeData,
|
||||
@@ -51,38 +50,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) ->
|
||||
client = OneDriveClient(get_access_token, async_get_clientsession(hass))
|
||||
|
||||
# get approot, will be created automatically if it does not exist
|
||||
approot = await _handle_item_operation(client.get_approot, "approot")
|
||||
folder_name = entry.data[CONF_FOLDER_NAME]
|
||||
|
||||
try:
|
||||
backup_folder = await _handle_item_operation(
|
||||
lambda: client.get_drive_item(path_or_id=entry.data[CONF_FOLDER_ID]),
|
||||
folder_name,
|
||||
)
|
||||
except NotFoundError:
|
||||
_LOGGER.debug("Creating backup folder %s", folder_name)
|
||||
backup_folder = await _handle_item_operation(
|
||||
lambda: client.create_folder(parent_id=approot.id, name=folder_name),
|
||||
folder_name,
|
||||
)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_FOLDER_ID: backup_folder.id}
|
||||
)
|
||||
approot = await client.get_approot()
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from err
|
||||
except (HttpRequestException, OneDriveException, TimeoutError) as err:
|
||||
_LOGGER.debug("Failed to get approot", exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="failed_to_get_folder",
|
||||
translation_placeholders={"folder": "approot"},
|
||||
) from err
|
||||
|
||||
# write instance id to description
|
||||
if backup_folder.description != (instance_id := await async_get_instance_id(hass)):
|
||||
await _handle_item_operation(
|
||||
lambda: client.update_drive_item(
|
||||
backup_folder.id, ItemUpdate(description=instance_id)
|
||||
),
|
||||
folder_name,
|
||||
)
|
||||
|
||||
# update in case folder was renamed manually inside OneDrive
|
||||
if backup_folder.name != entry.data[CONF_FOLDER_NAME]:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, CONF_FOLDER_NAME: backup_folder.name}
|
||||
instance_id = await async_get_instance_id(hass)
|
||||
backup_folder_name = f"backups_{instance_id[:8]}"
|
||||
try:
|
||||
backup_folder = await client.create_folder(
|
||||
parent_id=approot.id, name=backup_folder_name
|
||||
)
|
||||
except (HttpRequestException, OneDriveException, TimeoutError) as err:
|
||||
_LOGGER.debug("Failed to create backup folder", exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="failed_to_get_folder",
|
||||
translation_placeholders={"folder": backup_folder_name},
|
||||
) from err
|
||||
|
||||
coordinator = OneDriveUpdateCoordinator(hass, entry, client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -158,47 +152,3 @@ async def _migrate_backup_files(client: OneDriveClient, backup_folder_id: str) -
|
||||
data=ItemUpdate(description=""),
|
||||
)
|
||||
_LOGGER.debug("Migrated backup file %s", file.name)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
if entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if (version := entry.version) == 1 and (minor_version := entry.minor_version) == 1:
|
||||
_LOGGER.debug(
|
||||
"Migrating OneDrive config entry from version %s.%s", version, minor_version
|
||||
)
|
||||
|
||||
instance_id = await async_get_instance_id(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
CONF_FOLDER_ID: "id", # will be updated during setup_entry
|
||||
CONF_FOLDER_NAME: f"backups_{instance_id[:8]}",
|
||||
},
|
||||
)
|
||||
_LOGGER.debug("Migration to version 1.2 successful")
|
||||
return True
|
||||
|
||||
|
||||
async def _handle_item_operation(
|
||||
func: Callable[[], Awaitable[Item]], folder: str
|
||||
) -> Item:
|
||||
try:
|
||||
return await func()
|
||||
except NotFoundError:
|
||||
raise
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from err
|
||||
except (OneDriveException, TimeoutError) as err:
|
||||
_LOGGER.debug("Failed to get approot", exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="failed_to_get_folder",
|
||||
translation_placeholders={"folder": folder},
|
||||
) from err
|
||||
|
||||
@@ -74,7 +74,7 @@ def async_register_backup_agents_listener(
|
||||
def handle_backup_errors[_R, **P](
|
||||
func: Callable[Concatenate[OneDriveBackupAgent, P], Coroutine[Any, Any, _R]],
|
||||
) -> Callable[Concatenate[OneDriveBackupAgent, P], Coroutine[Any, Any, _R]]:
|
||||
"""Handle backup errors."""
|
||||
"""Handle backup errors with a specific translation key."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(
|
||||
|
||||
@@ -8,47 +8,22 @@ from typing import Any, cast
|
||||
|
||||
from onedrive_personal_sdk.clients.client import OneDriveClient
|
||||
from onedrive_personal_sdk.exceptions import OneDriveException
|
||||
from onedrive_personal_sdk.models.items import AppRoot, ItemUpdate
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
|
||||
|
||||
from .const import (
|
||||
CONF_DELETE_PERMANENTLY,
|
||||
CONF_FOLDER_ID,
|
||||
CONF_FOLDER_NAME,
|
||||
DOMAIN,
|
||||
OAUTH_SCOPES,
|
||||
)
|
||||
from .const import CONF_DELETE_PERMANENTLY, DOMAIN, OAUTH_SCOPES
|
||||
from .coordinator import OneDriveConfigEntry
|
||||
|
||||
FOLDER_NAME_SCHEMA = vol.Schema({vol.Required(CONF_FOLDER_NAME): str})
|
||||
|
||||
|
||||
class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Config flow to handle OneDrive OAuth2 authentication."""
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
MINOR_VERSION = 2
|
||||
|
||||
client: OneDriveClient
|
||||
approot: AppRoot
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the OneDrive config flow."""
|
||||
super().__init__()
|
||||
self.step_data: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
@@ -60,15 +35,6 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {"scope": " ".join(OAUTH_SCOPES)}
|
||||
|
||||
@property
|
||||
def apps_folder(self) -> str:
|
||||
"""Return the name of the Apps folder (translated)."""
|
||||
return (
|
||||
path.split("/")[-1]
|
||||
if (path := self.approot.parent_reference.path)
|
||||
else "Apps"
|
||||
)
|
||||
|
||||
async def async_oauth_create_entry(
|
||||
self,
|
||||
data: dict[str, Any],
|
||||
@@ -78,12 +44,12 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
async def get_access_token() -> str:
|
||||
return cast(str, data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||
|
||||
self.client = OneDriveClient(
|
||||
graph_client = OneDriveClient(
|
||||
get_access_token, async_get_clientsession(self.hass)
|
||||
)
|
||||
|
||||
try:
|
||||
self.approot = await self.client.get_approot()
|
||||
approot = await graph_client.get_approot()
|
||||
except OneDriveException:
|
||||
self.logger.exception("Failed to connect to OneDrive")
|
||||
return self.async_abort(reason="connection_error")
|
||||
@@ -91,118 +57,26 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
self.logger.exception("Unknown error")
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
await self.async_set_unique_id(self.approot.parent_reference.drive_id)
|
||||
|
||||
if self.source != SOURCE_USER:
|
||||
self._abort_if_unique_id_mismatch(
|
||||
reason="wrong_drive",
|
||||
)
|
||||
await self.async_set_unique_id(approot.parent_reference.drive_id)
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
self._abort_if_unique_id_mismatch(
|
||||
reason="wrong_drive",
|
||||
)
|
||||
return self.async_update_reload_and_abort(
|
||||
entry=reauth_entry,
|
||||
data=data,
|
||||
)
|
||||
|
||||
if self.source != SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_configured()
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self.step_data = data
|
||||
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
return await self.async_step_reconfigure_folder()
|
||||
|
||||
return await self.async_step_folder_name()
|
||||
|
||||
async def async_step_folder_name(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Step to ask for the folder name."""
|
||||
errors: dict[str, str] = {}
|
||||
instance_id = await async_get_instance_id(self.hass)
|
||||
if user_input is not None:
|
||||
try:
|
||||
folder = await self.client.create_folder(
|
||||
self.approot.id, user_input[CONF_FOLDER_NAME]
|
||||
)
|
||||
except OneDriveException:
|
||||
self.logger.debug("Failed to create folder", exc_info=True)
|
||||
errors["base"] = "folder_creation_error"
|
||||
else:
|
||||
if folder.description and folder.description != instance_id:
|
||||
errors[CONF_FOLDER_NAME] = "folder_already_in_use"
|
||||
if not errors:
|
||||
title = (
|
||||
f"{self.approot.created_by.user.display_name}'s OneDrive"
|
||||
if self.approot.created_by.user
|
||||
and self.approot.created_by.user.display_name
|
||||
else "OneDrive"
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data={
|
||||
**self.step_data,
|
||||
CONF_FOLDER_ID: folder.id,
|
||||
CONF_FOLDER_NAME: user_input[CONF_FOLDER_NAME],
|
||||
},
|
||||
)
|
||||
|
||||
default_folder_name = (
|
||||
f"backups_{instance_id[:8]}"
|
||||
if user_input is None
|
||||
else user_input[CONF_FOLDER_NAME]
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="folder_name",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
FOLDER_NAME_SCHEMA, {CONF_FOLDER_NAME: default_folder_name}
|
||||
),
|
||||
description_placeholders={
|
||||
"apps_folder": self.apps_folder,
|
||||
"approot": self.approot.name,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure_folder(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Reconfigure the folder name."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
if (
|
||||
new_folder_name := user_input[CONF_FOLDER_NAME]
|
||||
) != reconfigure_entry.data[CONF_FOLDER_NAME]:
|
||||
try:
|
||||
await self.client.update_drive_item(
|
||||
reconfigure_entry.data[CONF_FOLDER_ID],
|
||||
ItemUpdate(name=new_folder_name),
|
||||
)
|
||||
except OneDriveException:
|
||||
self.logger.debug("Failed to update folder", exc_info=True)
|
||||
errors["base"] = "folder_rename_error"
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data={**reconfigure_entry.data, CONF_FOLDER_NAME: new_folder_name},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure_folder",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
FOLDER_NAME_SCHEMA,
|
||||
{CONF_FOLDER_NAME: reconfigure_entry.data[CONF_FOLDER_NAME]},
|
||||
),
|
||||
description_placeholders={
|
||||
"apps_folder": self.apps_folder,
|
||||
"approot": self.approot.name,
|
||||
},
|
||||
errors=errors,
|
||||
title = (
|
||||
f"{approot.created_by.user.display_name}'s OneDrive"
|
||||
if approot.created_by.user and approot.created_by.user.display_name
|
||||
else "OneDrive"
|
||||
)
|
||||
return self.async_create_entry(title=title, data=data)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
@@ -218,12 +92,6 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Reconfigure the entry."""
|
||||
return await self.async_step_user()
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
||||
@@ -6,8 +6,6 @@ from typing import Final
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DOMAIN: Final = "onedrive"
|
||||
CONF_FOLDER_NAME: Final = "folder_name"
|
||||
CONF_FOLDER_ID: Final = "folder_id"
|
||||
|
||||
CONF_DELETE_PERMANENTLY: Final = "delete_permanently"
|
||||
|
||||
|
||||
@@ -73,7 +73,10 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
Nothing to reconfigure.
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: exempt
|
||||
|
||||
@@ -7,26 +7,6 @@
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The OneDrive integration needs to re-authenticate your account"
|
||||
},
|
||||
"folder_name": {
|
||||
"title": "Pick a folder name",
|
||||
"description": "This name will be used to create a folder that is specific for this Home Assistant instance. This folder will be created inside `{apps_folder}/{approot}`",
|
||||
"data": {
|
||||
"folder_name": "Folder name"
|
||||
},
|
||||
"data_description": {
|
||||
"folder_name": "Name of the folder"
|
||||
}
|
||||
},
|
||||
"reconfigure_folder": {
|
||||
"title": "Change the folder name",
|
||||
"description": "Rename the instance specific folder inside `{apps_folder}/{approot}`. This will only rename the folder (and does not select another folder), so make sure the new name is not already in use.",
|
||||
"data": {
|
||||
"folder_name": "[%key:component::onedrive::config::step::folder_name::data::folder_name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"folder_name": "[%key:component::onedrive::config::step::folder_name::data_description::folder_name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
@@ -43,16 +23,10 @@
|
||||
"connection_error": "Failed to connect to OneDrive.",
|
||||
"wrong_drive": "New account does not contain previously configured OneDrive.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
},
|
||||
"error": {
|
||||
"folder_rename_error": "Failed to rename folder",
|
||||
"folder_creation_error": "Failed to create folder",
|
||||
"folder_already_in_use": "Folder already used for backups from another Home Assistant instance"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -287,9 +287,6 @@ class OpenAIConversationEntity(
|
||||
|
||||
try:
|
||||
result = await client.chat.completions.create(**model_args)
|
||||
except openai.RateLimitError as err:
|
||||
LOGGER.error("Rate limited by OpenAI: %s", err)
|
||||
raise HomeAssistantError("Rate limited or insufficient funds") from err
|
||||
except openai.OpenAIError as err:
|
||||
LOGGER.error("Error talking to OpenAI: %s", err)
|
||||
raise HomeAssistantError("Error talking to OpenAI") from err
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/prosegur",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyprosegur"],
|
||||
"requirements": ["pyprosegur==0.0.14"]
|
||||
"requirements": ["pyprosegur==0.0.9"]
|
||||
}
|
||||
|
||||
@@ -59,11 +59,14 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
|
||||
async def _async_update_data(self) -> PyLoadData:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
if not self.version:
|
||||
self.version = await self.pyload.version()
|
||||
return PyLoadData(
|
||||
**await self.pyload.get_status(),
|
||||
free_space=await self.pyload.free_space(),
|
||||
)
|
||||
except InvalidAuth:
|
||||
|
||||
except InvalidAuth as e:
|
||||
try:
|
||||
await self.pyload.login()
|
||||
except InvalidAuth as exc:
|
||||
@@ -72,10 +75,10 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
|
||||
translation_key="setup_authentication_exception",
|
||||
translation_placeholders={CONF_USERNAME: self.pyload.username},
|
||||
) from exc
|
||||
_LOGGER.debug(
|
||||
"Unable to retrieve data due to cookie expiration, retrying after 20 seconds"
|
||||
)
|
||||
return self.data
|
||||
|
||||
raise UpdateFailed(
|
||||
"Unable to retrieve data due to cookie expiration"
|
||||
) from e
|
||||
except CannotConnect as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from pyLoad API"
|
||||
@@ -88,7 +91,6 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
|
||||
|
||||
try:
|
||||
await self.pyload.login()
|
||||
self.version = await self.pyload.version()
|
||||
except CannotConnect as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"cloudapp/QBUSMQTTGW/+/state"
|
||||
],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["qbusmqttapi==1.3.0"]
|
||||
"requirements": ["qbusmqttapi==1.2.4"]
|
||||
}
|
||||
|
||||
@@ -50,11 +50,6 @@ STATES_META_SCHEMA_VERSION = 38
|
||||
LAST_REPORTED_SCHEMA_VERSION = 43
|
||||
|
||||
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28
|
||||
LEGACY_STATES_EVENT_FOREIGN_KEYS_FIXED_SCHEMA_VERSION = 43
|
||||
# https://github.com/home-assistant/core/pull/120779
|
||||
# fixed the foreign keys in the states table but it did
|
||||
# not bump the schema version which means only databases
|
||||
# created with schema 44 and later do not need the rebuild.
|
||||
|
||||
INTEGRATION_PLATFORM_COMPILE_STATISTICS = "compile_statistics"
|
||||
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS = "list_statistic_ids"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"SQLAlchemy==2.0.38",
|
||||
"fnv-hash-fast==1.2.3",
|
||||
"fnv-hash-fast==1.2.2",
|
||||
"psutil-home-assistant==0.0.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ from .auto_repairs.statistics.schema import (
|
||||
from .const import (
|
||||
CONTEXT_ID_AS_BINARY_SCHEMA_VERSION,
|
||||
EVENT_TYPE_IDS_SCHEMA_VERSION,
|
||||
LEGACY_STATES_EVENT_FOREIGN_KEYS_FIXED_SCHEMA_VERSION,
|
||||
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION,
|
||||
STATES_META_SCHEMA_VERSION,
|
||||
SupportedDialect,
|
||||
@@ -2491,10 +2490,9 @@ class BaseMigration(ABC):
|
||||
if self.initial_schema_version > self.max_initial_schema_version:
|
||||
_LOGGER.debug(
|
||||
"Data migration '%s' not needed, database created with version %s "
|
||||
"after migrator was added in version %s",
|
||||
"after migrator was added",
|
||||
self.migration_id,
|
||||
self.initial_schema_version,
|
||||
self.max_initial_schema_version,
|
||||
)
|
||||
return False
|
||||
if self.start_schema_version < self.required_schema_version:
|
||||
@@ -2870,14 +2868,7 @@ class EventIDPostMigration(BaseRunTimeMigration):
|
||||
"""Migration to remove old event_id index from states."""
|
||||
|
||||
migration_id = "event_id_post_migration"
|
||||
# Note we don't subtract 1 from the max_initial_schema_version
|
||||
# in this case because we need to run this migration on databases
|
||||
# version >= 43 because the schema was not bumped when the table
|
||||
# rebuild was added in
|
||||
# https://github.com/home-assistant/core/pull/120779
|
||||
# which means its only safe to assume version 44 and later
|
||||
# do not need the table rebuild
|
||||
max_initial_schema_version = LEGACY_STATES_EVENT_FOREIGN_KEYS_FIXED_SCHEMA_VERSION
|
||||
max_initial_schema_version = LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION - 1
|
||||
task = MigrationTask
|
||||
migration_version = 2
|
||||
|
||||
|
||||
@@ -60,21 +60,20 @@ class RememberTheMilkEntity(Entity):
|
||||
result = self._rtm_api.rtm.timelines.create()
|
||||
timeline = result.timeline.value
|
||||
|
||||
if rtm_id is None:
|
||||
if hass_id is None or rtm_id is None:
|
||||
result = self._rtm_api.rtm.tasks.add(
|
||||
timeline=timeline, name=task_name, parse="1"
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Created new task '%s' in account %s", task_name, self.name
|
||||
)
|
||||
if hass_id is not None:
|
||||
self._rtm_config.set_rtm_id(
|
||||
self._name,
|
||||
hass_id,
|
||||
result.list.id,
|
||||
result.list.taskseries.id,
|
||||
result.list.taskseries.task.id,
|
||||
)
|
||||
self._rtm_config.set_rtm_id(
|
||||
self._name,
|
||||
hass_id,
|
||||
result.list.id,
|
||||
result.list.taskseries.id,
|
||||
result.list.taskseries.task.id,
|
||||
)
|
||||
else:
|
||||
self._rtm_api.rtm.tasks.setName(
|
||||
name=task_name,
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.12.0"]
|
||||
"requirements": ["reolink-aio==0.11.10"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/risco",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyrisco"],
|
||||
"requirements": ["pyrisco==0.6.7"]
|
||||
"requirements": ["pyrisco==0.6.5"]
|
||||
}
|
||||
|
||||
@@ -42,12 +42,6 @@ class RymProDataUpdateCoordinator(DataUpdateCoordinator[dict[int, dict]]):
|
||||
try:
|
||||
meters = await self.rympro.last_read()
|
||||
for meter_id, meter in meters.items():
|
||||
meter["monthly_consumption"] = await self.rympro.monthly_consumption(
|
||||
meter_id
|
||||
)
|
||||
meter["daily_consumption"] = await self.rympro.daily_consumption(
|
||||
meter_id
|
||||
)
|
||||
meter["consumption_forecast"] = await self.rympro.consumption_forecast(
|
||||
meter_id
|
||||
)
|
||||
|
||||
@@ -36,20 +36,6 @@ SENSOR_DESCRIPTIONS: tuple[RymProSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=3,
|
||||
value_key="read",
|
||||
),
|
||||
RymProSensorEntityDescription(
|
||||
key="monthly_consumption",
|
||||
translation_key="monthly_consumption",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=3,
|
||||
value_key="monthly_consumption",
|
||||
),
|
||||
RymProSensorEntityDescription(
|
||||
key="daily_consumption",
|
||||
translation_key="daily_consumption",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=3,
|
||||
value_key="daily_consumption",
|
||||
),
|
||||
RymProSensorEntityDescription(
|
||||
key="monthly_forecast",
|
||||
translation_key="monthly_forecast",
|
||||
|
||||
@@ -23,12 +23,6 @@
|
||||
"total_consumption": {
|
||||
"name": "Total consumption"
|
||||
},
|
||||
"monthly_consumption": {
|
||||
"name": "Monthly consumption"
|
||||
},
|
||||
"daily_consumption": {
|
||||
"name": "Daily consumption"
|
||||
},
|
||||
"monthly_forecast": {
|
||||
"name": "Monthly forecast"
|
||||
}
|
||||
|
||||
@@ -68,11 +68,11 @@ from .utils import (
|
||||
async_create_issue_unsupported_firmware,
|
||||
get_block_device_sleep_period,
|
||||
get_device_entry_gen,
|
||||
get_device_info_model,
|
||||
get_host,
|
||||
get_http_port,
|
||||
get_rpc_device_wakeup_period,
|
||||
get_rpc_ws_url,
|
||||
get_shelly_model_name,
|
||||
update_device_fw_info,
|
||||
)
|
||||
|
||||
@@ -165,7 +165,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
connections={(CONNECTION_NETWORK_MAC, self.mac)},
|
||||
identifiers={(DOMAIN, self.mac)},
|
||||
manufacturer="Shelly",
|
||||
model=get_shelly_model_name(self.model, self.sleep_period, self.device),
|
||||
model=get_device_info_model(self.device),
|
||||
model_id=self.model,
|
||||
sw_version=self.sw_version,
|
||||
hw_version=f"gen{get_device_entry_gen(self.config_entry)}",
|
||||
|
||||
@@ -315,25 +315,12 @@ def get_model_name(info: dict[str, Any]) -> str:
|
||||
return cast(str, MODEL_NAMES.get(info["type"], info["type"]))
|
||||
|
||||
|
||||
def get_shelly_model_name(
|
||||
model: str,
|
||||
sleep_period: int,
|
||||
device: BlockDevice | RpcDevice,
|
||||
) -> str | None:
|
||||
"""Get Shelly model name.
|
||||
def get_device_info_model(device: BlockDevice | RpcDevice) -> str | None:
|
||||
"""Return the device model for deviceinfo."""
|
||||
if isinstance(device, RpcDevice) and (model := device.xmod_info.get("n")):
|
||||
return cast(str, model)
|
||||
|
||||
Assume that XMOD devices are not sleepy devices.
|
||||
"""
|
||||
if (
|
||||
sleep_period == 0
|
||||
and isinstance(device, RpcDevice)
|
||||
and (model_name := device.xmod_info.get("n"))
|
||||
):
|
||||
# Use the model name from XMOD data
|
||||
return cast(str, model_name)
|
||||
|
||||
# Use the model name from aioshelly
|
||||
return cast(str, MODEL_NAMES.get(model))
|
||||
return cast(str, MODEL_NAMES.get(device.model))
|
||||
|
||||
|
||||
def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""Support for the Swedish weather institute weather base entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
from pysmhi import SMHIPointForecast
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class SmhiWeatherBaseEntity(Entity):
|
||||
"""Representation of a base weather entity."""
|
||||
|
||||
_attr_attribution = "Swedish weather institute (SMHI)"
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
latitude: str,
|
||||
longitude: str,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the SMHI base weather entity."""
|
||||
self._attr_unique_id = f"{latitude}, {longitude}"
|
||||
self._smhi_api = SMHIPointForecast(longitude, latitude, session=session)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, f"{latitude}, {longitude}")},
|
||||
manufacturer="SMHI",
|
||||
model="v2",
|
||||
configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html",
|
||||
)
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
from typing import Any, Final
|
||||
|
||||
import aiohttp
|
||||
from pysmhi import SMHIForecast, SmhiForecastException
|
||||
from pysmhi import SMHIForecast, SmhiForecastException, SMHIPointForecast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
@@ -55,12 +55,12 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client, sun
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import ATTR_SMHI_THUNDER_PROBABILITY, ENTITY_ID_SENSOR_FORMAT
|
||||
from .entity import SmhiWeatherBaseEntity
|
||||
from .const import ATTR_SMHI_THUNDER_PROBABILITY, DOMAIN, ENTITY_ID_SENSOR_FORMAT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -114,14 +114,18 @@ async def async_setup_entry(
|
||||
async_add_entities([entity], True)
|
||||
|
||||
|
||||
class SmhiWeather(SmhiWeatherBaseEntity, WeatherEntity):
|
||||
class SmhiWeather(WeatherEntity):
|
||||
"""Representation of a weather entity."""
|
||||
|
||||
_attr_attribution = "Swedish weather institute (SMHI)"
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_visibility_unit = UnitOfLength.KILOMETERS
|
||||
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
|
||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||
)
|
||||
@@ -133,10 +137,18 @@ class SmhiWeather(SmhiWeatherBaseEntity, WeatherEntity):
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the SMHI weather entity."""
|
||||
super().__init__(latitude, longitude, session)
|
||||
self._attr_unique_id = f"{latitude}, {longitude}"
|
||||
self._forecast_daily: list[SMHIForecast] | None = None
|
||||
self._forecast_hourly: list[SMHIForecast] | None = None
|
||||
self._fail_count = 0
|
||||
self._smhi_api = SMHIPointForecast(longitude, latitude, session=session)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, f"{latitude}, {longitude}")},
|
||||
manufacturer="SMHI",
|
||||
model="v2",
|
||||
configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html",
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
|
||||
@@ -9,6 +9,7 @@ from stookwijzer import Stookwijzer
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er, issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import StookwijzerConfigEntry, StookwijzerCoordinator
|
||||
@@ -43,6 +44,7 @@ async def async_migrate_entry(
|
||||
|
||||
if entry.version == 1:
|
||||
latitude, longitude = await Stookwijzer.async_transform_coordinates(
|
||||
async_get_clientsession(hass),
|
||||
entry.data[CONF_LOCATION][CONF_LATITUDE],
|
||||
entry.data[CONF_LOCATION][CONF_LONGITUDE],
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import LocationSelector
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -26,6 +27,7 @@ class StookwijzerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
latitude, longitude = await Stookwijzer.async_transform_coordinates(
|
||||
async_get_clientsession(self.hass),
|
||||
user_input[CONF_LOCATION][CONF_LATITUDE],
|
||||
user_input[CONF_LOCATION][CONF_LONGITUDE],
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/stookwijzer",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["stookwijzer==1.5.7"]
|
||||
"requirements": ["stookwijzer==1.5.4"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from synology_dsm import SynologyDSM
|
||||
from synology_dsm.api.core.security import SynoCoreSecurity
|
||||
from synology_dsm.api.core.system import SynoCoreSystem
|
||||
@@ -136,9 +135,6 @@ class SynoApi:
|
||||
)
|
||||
await self.async_login()
|
||||
|
||||
self.information = self.dsm.information
|
||||
await self.information.update()
|
||||
|
||||
# check if surveillance station is used
|
||||
self._with_surveillance_station = bool(
|
||||
self.dsm.apis.get(SynoSurveillanceStation.CAMERA_API_KEY)
|
||||
@@ -169,10 +165,7 @@ class SynoApi:
|
||||
LOGGER.debug("Disabled fetching upgrade data during setup: %s", ex)
|
||||
|
||||
# check if file station is used and permitted
|
||||
self._with_file_station = bool(
|
||||
self.information.awesome_version >= AwesomeVersion("6.0")
|
||||
and self.dsm.apis.get(SynoFileStation.LIST_API_KEY)
|
||||
)
|
||||
self._with_file_station = bool(self.dsm.apis.get(SynoFileStation.LIST_API_KEY))
|
||||
if self._with_file_station:
|
||||
shares: list | None = None
|
||||
with suppress(*SYNOLOGY_CONNECTION_EXCEPTIONS):
|
||||
@@ -324,6 +317,7 @@ class SynoApi:
|
||||
|
||||
async def _fetch_device_configuration(self) -> None:
|
||||
"""Fetch initial device config."""
|
||||
self.information = self.dsm.information
|
||||
self.network = self.dsm.network
|
||||
await self.network.update()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.7.0"],
|
||||
"requirements": ["py-synologydsm-api==2.6.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Synology",
|
||||
|
||||
@@ -72,7 +72,7 @@ class ThermoBeaconConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=self._discovered_devices[address], data={}
|
||||
)
|
||||
|
||||
current_addresses = self._async_current_ids(include_ignore=False)
|
||||
current_addresses = self._async_current_ids()
|
||||
for discovery_info in async_discovered_service_info(self.hass, False):
|
||||
address = discovery_info.address
|
||||
if address in current_addresses or address in self._discovered_devices:
|
||||
|
||||
@@ -2,47 +2,25 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from thermopro_ble import SensorUpdate, ThermoProBluetoothDeviceData
|
||||
from thermopro_ble import ThermoProBluetoothDeviceData
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
)
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothProcessorCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import DOMAIN, SIGNAL_DATA_UPDATED
|
||||
from .const import DOMAIN
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.SENSOR]
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_service_info(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
data: ThermoProBluetoothDeviceData,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
) -> SensorUpdate:
|
||||
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
|
||||
update = data.update(service_info)
|
||||
async_dispatcher_send(
|
||||
hass, f"{SIGNAL_DATA_UPDATED}_{entry.entry_id}", data, service_info, update
|
||||
)
|
||||
return update
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up ThermoPro BLE device from a config entry."""
|
||||
address = entry.unique_id
|
||||
@@ -54,12 +32,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER,
|
||||
address=address,
|
||||
mode=BluetoothScanningMode.ACTIVE,
|
||||
update_method=partial(process_service_info, hass, entry, data),
|
||||
update_method=data.update,
|
||||
)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
# only start after all platforms have had a chance to subscribe
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
entry.async_on_unload(
|
||||
coordinator.async_start()
|
||||
) # only start after all platforms have had a chance to subscribe
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
"""Thermopro button platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from thermopro_ble import SensorUpdate, ThermoProBluetoothDeviceData, ThermoProDevice
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_ble_device_from_address,
|
||||
async_track_unavailable,
|
||||
)
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
from .const import DOMAIN, SIGNAL_AVAILABILITY_UPDATED, SIGNAL_DATA_UPDATED
|
||||
|
||||
PARALLEL_UPDATES = 1 # one connection at a time
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class ThermoProButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describe a ThermoPro button entity."""
|
||||
|
||||
press_action_fn: Callable[[HomeAssistant, str], Coroutine[None, Any, Any]]
|
||||
|
||||
|
||||
async def _async_set_datetime(hass: HomeAssistant, address: str) -> None:
|
||||
"""Set Date&Time for a given device."""
|
||||
ble_device = async_ble_device_from_address(hass, address, connectable=True)
|
||||
assert ble_device is not None
|
||||
await ThermoProDevice(ble_device).set_datetime(now(), am_pm=False)
|
||||
|
||||
|
||||
BUTTON_ENTITIES: tuple[ThermoProButtonEntityDescription, ...] = (
|
||||
ThermoProButtonEntityDescription(
|
||||
key="datetime",
|
||||
translation_key="set_datetime",
|
||||
icon="mdi:calendar-clock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action_fn=_async_set_datetime,
|
||||
),
|
||||
)
|
||||
|
||||
MODELS_THAT_SUPPORT_BUTTONS = {"TP358", "TP393"}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the thermopro button platform."""
|
||||
address = entry.unique_id
|
||||
assert address is not None
|
||||
availability_signal = f"{SIGNAL_AVAILABILITY_UPDATED}_{entry.entry_id}"
|
||||
entity_added = False
|
||||
|
||||
@callback
|
||||
def _async_on_data_updated(
|
||||
data: ThermoProBluetoothDeviceData,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
update: SensorUpdate,
|
||||
) -> None:
|
||||
nonlocal entity_added
|
||||
sensor_device_info = update.devices[data.primary_device_id]
|
||||
if sensor_device_info.model not in MODELS_THAT_SUPPORT_BUTTONS:
|
||||
return
|
||||
|
||||
if not entity_added:
|
||||
name = sensor_device_info.name
|
||||
assert name is not None
|
||||
entity_added = True
|
||||
async_add_entities(
|
||||
ThermoProButtonEntity(
|
||||
description=description,
|
||||
data=data,
|
||||
availability_signal=availability_signal,
|
||||
address=address,
|
||||
)
|
||||
for description in BUTTON_ENTITIES
|
||||
)
|
||||
|
||||
if service_info.connectable:
|
||||
async_dispatcher_send(hass, availability_signal, True)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, f"{SIGNAL_DATA_UPDATED}_{entry.entry_id}", _async_on_data_updated
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ThermoProButtonEntity(ButtonEntity):
|
||||
"""Representation of a ThermoPro button entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: ThermoProButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
description: ThermoProButtonEntityDescription,
|
||||
data: ThermoProBluetoothDeviceData,
|
||||
availability_signal: str,
|
||||
address: str,
|
||||
) -> None:
|
||||
"""Initialize the thermopro button entity."""
|
||||
self.entity_description = description
|
||||
self._address = address
|
||||
self._availability_signal = availability_signal
|
||||
self._attr_unique_id = f"{address}-{description.key}"
|
||||
self._attr_device_info = dr.DeviceInfo(
|
||||
name=data.get_device_name(),
|
||||
identifiers={(DOMAIN, address)},
|
||||
connections={(dr.CONNECTION_BLUETOOTH, address)},
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Connect availability dispatcher."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
self._availability_signal,
|
||||
self._async_on_availability_changed,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_track_unavailable(
|
||||
self.hass, self._async_on_unavailable, self._address, connectable=True
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_on_unavailable(self, _: BluetoothServiceInfoBleak) -> None:
|
||||
self._async_on_availability_changed(False)
|
||||
|
||||
@callback
|
||||
def _async_on_availability_changed(self, available: bool) -> None:
|
||||
self._attr_available = available
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Execute the press action for the entity."""
|
||||
await self.entity_description.press_action_fn(self.hass, self._address)
|
||||
@@ -1,6 +1,3 @@
|
||||
"""Constants for the ThermoPro Bluetooth integration."""
|
||||
|
||||
DOMAIN = "thermopro"
|
||||
|
||||
SIGNAL_DATA_UPDATED = f"{DOMAIN}_service_info_updated"
|
||||
SIGNAL_AVAILABILITY_UPDATED = f"{DOMAIN}_availability_updated"
|
||||
|
||||
@@ -9,6 +9,7 @@ from thermopro_ble import (
|
||||
Units,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
PassiveBluetoothDataProcessor,
|
||||
PassiveBluetoothDataUpdate,
|
||||
@@ -22,7 +23,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
@@ -110,7 +110,7 @@ def sensor_update_to_bluetooth_data_update(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: config_entries.ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the ThermoPro BLE sensors."""
|
||||
|
||||
@@ -17,12 +17,5 @@
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"set_datetime": {
|
||||
"name": "Set Date&Time"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +101,6 @@ class TVTrainConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
_from_stations: list[StationInfoModel]
|
||||
_to_stations: list[StationInfoModel]
|
||||
_time: str | None
|
||||
_days: list
|
||||
_product: str | None
|
||||
_data: dict[str, Any]
|
||||
|
||||
@staticmethod
|
||||
@@ -246,10 +243,8 @@ class TVTrainConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the select station step."""
|
||||
if user_input is not None:
|
||||
api_key: str = self._data[CONF_API_KEY]
|
||||
train_from: str = (
|
||||
user_input.get(CONF_FROM) or self._from_stations[0].signature
|
||||
)
|
||||
train_to: str = user_input.get(CONF_TO) or self._to_stations[0].signature
|
||||
train_from: str = user_input[CONF_FROM]
|
||||
train_to: str = user_input[CONF_TO]
|
||||
train_time: str | None = self._data.get(CONF_TIME)
|
||||
train_days: list = self._data[CONF_WEEKDAY]
|
||||
filter_product: str | None = self._data[CONF_FILTER_PRODUCT]
|
||||
|
||||
@@ -46,7 +46,6 @@ class UnifiEntityLoader:
|
||||
hub.api.port_forwarding.update,
|
||||
hub.api.sites.update,
|
||||
hub.api.system_information.update,
|
||||
hub.api.firewall_policies.update,
|
||||
hub.api.traffic_rules.update,
|
||||
hub.api.traffic_routes.update,
|
||||
hub.api.wlans.update,
|
||||
|
||||
@@ -55,9 +55,6 @@
|
||||
"off": "mdi:network-off"
|
||||
}
|
||||
},
|
||||
"firewall_policy_control": {
|
||||
"default": "mdi:security-network"
|
||||
},
|
||||
"port_forward_control": {
|
||||
"default": "mdi:upload-network"
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"requirements": ["aiounifi==82"],
|
||||
"requirements": ["aiounifi==81"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from aiounifi.models.client import ClientReconnectRequest, ClientRemoveRequest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -66,9 +67,9 @@ async def async_reconnect_client(hass: HomeAssistant, data: Mapping[str, Any]) -
|
||||
if mac == "":
|
||||
return
|
||||
|
||||
for config_entry in hass.config_entries.async_loaded_entries(UNIFI_DOMAIN):
|
||||
if (
|
||||
(not (hub := config_entry.runtime_data).available)
|
||||
for config_entry in hass.config_entries.async_entries(UNIFI_DOMAIN):
|
||||
if config_entry.state is not ConfigEntryState.LOADED or (
|
||||
((hub := config_entry.runtime_data) and not hub.available)
|
||||
or (client := hub.api.clients.get(mac)) is None
|
||||
or client.is_wired
|
||||
):
|
||||
@@ -84,8 +85,10 @@ async def async_remove_clients(hass: HomeAssistant, data: Mapping[str, Any]) ->
|
||||
- Total time between first seen and last seen is less than 15 minutes.
|
||||
- Neither IP, hostname nor name is configured.
|
||||
"""
|
||||
for config_entry in hass.config_entries.async_loaded_entries(UNIFI_DOMAIN):
|
||||
if not (hub := config_entry.runtime_data).available:
|
||||
for config_entry in hass.config_entries.async_entries(UNIFI_DOMAIN):
|
||||
if config_entry.state is not ConfigEntryState.LOADED or (
|
||||
(hub := config_entry.runtime_data) and not hub.available
|
||||
):
|
||||
continue
|
||||
|
||||
clients_to_remove = []
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user