mirror of
https://github.com/home-assistant/core.git
synced 2025-11-10 19:40:11 +00:00
Compare commits
18 Commits
copilot/ad
...
add-includ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49db8800cc | ||
|
|
a3752e1ee3 | ||
|
|
a1eada037a | ||
|
|
ba076de313 | ||
|
|
5f18d60afb | ||
|
|
341d38e961 | ||
|
|
706abf52c2 | ||
|
|
74c86ac0c1 | ||
|
|
388a396519 | ||
|
|
f9d2d69d78 | ||
|
|
eab86c4320 | ||
|
|
e365c86c92 | ||
|
|
8e7ffd7d90 | ||
|
|
1318cad084 | ||
|
|
7e81e406ac | ||
|
|
1fe3892c83 | ||
|
|
89aed81ae0 | ||
|
|
f3a2d060a5 |
@@ -6,6 +6,7 @@ Sending HOTP through notify service
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -303,14 +304,13 @@ class NotifySetupFlow(SetupFlow[NotifyAuthModule]):
|
||||
if not self._available_notify_services:
|
||||
return self.async_abort(reason="no_available_service")
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required("notify_service"): vol.In(self._available_notify_services),
|
||||
vol.Optional("target"): str,
|
||||
}
|
||||
)
|
||||
schema: dict[str, Any] = OrderedDict()
|
||||
schema["notify_service"] = vol.In(self._available_notify_services)
|
||||
schema["target"] = vol.Optional(str)
|
||||
|
||||
return self.async_show_form(step_id="init", data_schema=schema, errors=errors)
|
||||
return self.async_show_form(
|
||||
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||
)
|
||||
|
||||
async def async_step_setup(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
|
||||
@@ -17,11 +17,6 @@ from homeassistant.const import (
|
||||
CONF_UNIQUE_ID,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ACCOUNT_ID,
|
||||
@@ -71,15 +66,7 @@ class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the local step."""
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(WIFI_SSID): str,
|
||||
vol.Required(WIFI_PSWD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
),
|
||||
),
|
||||
}
|
||||
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}
|
||||
)
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -22,74 +20,44 @@ from .const import CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class AdaxSensorDescription(SensorEntityDescription):
|
||||
"""Describes Adax sensor entity."""
|
||||
|
||||
data_key: str
|
||||
|
||||
|
||||
SENSORS: tuple[AdaxSensorDescription, ...] = (
|
||||
AdaxSensorDescription(
|
||||
key="temperature",
|
||||
data_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
AdaxSensorDescription(
|
||||
key="energy",
|
||||
data_key="energyWh",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=3,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdaxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax sensors with config flow."""
|
||||
"""Set up the Adax energy sensors with config flow."""
|
||||
if entry.data.get(CONNECTION_TYPE) != LOCAL:
|
||||
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
||||
|
||||
# Create individual energy sensors for each device
|
||||
async_add_entities(
|
||||
[
|
||||
AdaxSensor(cloud_coordinator, entity_description, device_id)
|
||||
for device_id in cloud_coordinator.data
|
||||
for entity_description in SENSORS
|
||||
]
|
||||
AdaxEnergySensor(cloud_coordinator, device_id)
|
||||
for device_id in cloud_coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AdaxSensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
||||
"""Representation of an Adax sensor."""
|
||||
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
||||
"""Representation of an Adax energy sensor."""
|
||||
|
||||
entity_description: AdaxSensorDescription
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "energy"
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
|
||||
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
_attr_suggested_display_precision = 3
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AdaxCloudCoordinator,
|
||||
entity_description: AdaxSensorDescription,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
"""Initialize the energy sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._device_id = device_id
|
||||
room = coordinator.data[device_id]
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{room['homeId']}_{device_id}_{self.entity_description.key}"
|
||||
)
|
||||
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=room["name"],
|
||||
@@ -100,14 +68,10 @@ class AdaxSensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and self.entity_description.data_key
|
||||
in self.coordinator.data[self._device_id]
|
||||
super().available and "energyWh" in self.coordinator.data[self._device_id]
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | float | None:
|
||||
def native_value(self) -> int:
|
||||
"""Return the native value of the sensor."""
|
||||
return self.coordinator.data[self._device_id].get(
|
||||
self.entity_description.data_key
|
||||
)
|
||||
return int(self.coordinator.data[self._device_id]["energyWh"])
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import anthropic
|
||||
import voluptuous as vol
|
||||
@@ -37,7 +38,6 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorConfig,
|
||||
TemplateSelector,
|
||||
)
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
@@ -55,7 +55,6 @@ from .const import (
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
DEFAULT_CONVERSATION_NAME,
|
||||
DOMAIN,
|
||||
NON_THINKING_MODELS,
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
RECOMMENDED_MAX_TOKENS,
|
||||
RECOMMENDED_TEMPERATURE,
|
||||
@@ -153,218 +152,95 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Flow for managing conversation subentries."""
|
||||
|
||||
options: dict[str, Any]
|
||||
last_rendered_recommended = False
|
||||
|
||||
@property
|
||||
def _is_new(self) -> bool:
|
||||
"""Return if this is a new subentry."""
|
||||
return self.source == "user"
|
||||
|
||||
async def async_step_user(
|
||||
async def async_step_set_options(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Add a subentry."""
|
||||
self.options = RECOMMENDED_OPTIONS.copy()
|
||||
return await self.async_step_init()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle reconfiguration of a subentry."""
|
||||
self.options = self._get_reconfigure_subentry().data.copy()
|
||||
return await self.async_step_init()
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Set initial options."""
|
||||
"""Set conversation options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(self.hass)
|
||||
]
|
||||
if (suggested_llm_apis := self.options.get(CONF_LLM_HASS_API)) and isinstance(
|
||||
suggested_llm_apis, str
|
||||
):
|
||||
self.options[CONF_LLM_HASS_API] = [suggested_llm_apis]
|
||||
|
||||
step_schema: VolDictType = {}
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if self._is_new:
|
||||
step_schema[vol.Required(CONF_NAME, default=DEFAULT_CONVERSATION_NAME)] = (
|
||||
str
|
||||
if user_input is None:
|
||||
if self._is_new:
|
||||
options = RECOMMENDED_OPTIONS.copy()
|
||||
else:
|
||||
# If this is a reconfiguration, we need to copy the existing options
|
||||
# so that we can show the current values in the form.
|
||||
options = self._get_reconfigure_subentry().data.copy()
|
||||
|
||||
self.last_rendered_recommended = cast(
|
||||
bool, options.get(CONF_RECOMMENDED, False)
|
||||
)
|
||||
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(CONF_PROMPT): TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(options=hass_apis, multiple=True)
|
||||
),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=self.options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
if user_input is not None:
|
||||
elif user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if not user_input.get(CONF_LLM_HASS_API):
|
||||
user_input.pop(CONF_LLM_HASS_API, None)
|
||||
|
||||
if user_input[CONF_RECOMMENDED]:
|
||||
if not errors:
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=user_input.pop(CONF_NAME),
|
||||
data=user_input,
|
||||
)
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=user_input,
|
||||
)
|
||||
else:
|
||||
self.options.update(user_input)
|
||||
if (
|
||||
CONF_LLM_HASS_API in self.options
|
||||
and CONF_LLM_HASS_API not in user_input
|
||||
):
|
||||
self.options.pop(CONF_LLM_HASS_API)
|
||||
if not errors:
|
||||
return await self.async_step_advanced()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(step_schema), self.options
|
||||
),
|
||||
errors=errors or None,
|
||||
)
|
||||
|
||||
async def async_step_advanced(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Manage advanced options."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
step_schema: VolDictType = {
|
||||
vol.Optional(
|
||||
CONF_CHAT_MODEL,
|
||||
default=RECOMMENDED_CHAT_MODEL,
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=RECOMMENDED_MAX_TOKENS,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=RECOMMENDED_TEMPERATURE,
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
||||
}
|
||||
|
||||
if user_input is not None:
|
||||
self.options.update(user_input)
|
||||
|
||||
if not errors:
|
||||
return await self.async_step_model()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="advanced",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(step_schema), self.options
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_model(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Manage model-specific options."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
step_schema: VolDictType = {}
|
||||
|
||||
model = self.options[CONF_CHAT_MODEL]
|
||||
|
||||
if not model.startswith(tuple(NON_THINKING_MODELS)):
|
||||
step_schema[
|
||||
vol.Optional(CONF_THINKING_BUDGET, default=RECOMMENDED_THINKING_BUDGET)
|
||||
] = NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=0, max=self.options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS)
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.options.pop(CONF_THINKING_BUDGET, None)
|
||||
|
||||
if not model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
|
||||
step_schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH,
|
||||
default=RECOMMENDED_WEB_SEARCH,
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_MAX_USES,
|
||||
default=RECOMMENDED_WEB_SEARCH_MAX_USES,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
default=RECOMMENDED_WEB_SEARCH_USER_LOCATION,
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.options.pop(CONF_WEB_SEARCH, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_MAX_USES, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_USER_LOCATION, None)
|
||||
|
||||
self.options.pop(CONF_WEB_SEARCH_CITY, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_REGION, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_COUNTRY, None)
|
||||
self.options.pop(CONF_WEB_SEARCH_TIMEZONE, None)
|
||||
|
||||
if not step_schema:
|
||||
user_input = {}
|
||||
|
||||
if user_input is not None:
|
||||
if user_input.get(CONF_WEB_SEARCH, RECOMMENDED_WEB_SEARCH) and not errors:
|
||||
if user_input.get(
|
||||
if user_input.get(
|
||||
CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET
|
||||
) >= user_input.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS):
|
||||
errors[CONF_THINKING_BUDGET] = "thinking_budget_too_large"
|
||||
if user_input.get(CONF_WEB_SEARCH, RECOMMENDED_WEB_SEARCH):
|
||||
model = user_input.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
if model.startswith(tuple(WEB_SEARCH_UNSUPPORTED_MODELS)):
|
||||
errors[CONF_WEB_SEARCH] = "web_search_unsupported_model"
|
||||
elif user_input.get(
|
||||
CONF_WEB_SEARCH_USER_LOCATION, RECOMMENDED_WEB_SEARCH_USER_LOCATION
|
||||
):
|
||||
user_input.update(await self._get_location_data())
|
||||
|
||||
self.options.update(user_input)
|
||||
|
||||
if not errors:
|
||||
if self._is_new:
|
||||
return self.async_create_entry(
|
||||
title=self.options.pop(CONF_NAME),
|
||||
data=self.options,
|
||||
title=user_input.pop(CONF_NAME),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
self._get_reconfigure_subentry(),
|
||||
data=self.options,
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="model",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(step_schema), self.options
|
||||
options = user_input
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
else:
|
||||
# Re-render the options again, now with the recommended options shown/hidden
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input.get(CONF_LLM_HASS_API),
|
||||
}
|
||||
|
||||
suggested_values = options.copy()
|
||||
if not suggested_values.get(CONF_PROMPT):
|
||||
suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT
|
||||
if (
|
||||
suggested_llm_apis := suggested_values.get(CONF_LLM_HASS_API)
|
||||
) and isinstance(suggested_llm_apis, str):
|
||||
suggested_values[CONF_LLM_HASS_API] = [suggested_llm_apis]
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
anthropic_config_option_schema(self.hass, self._is_new, options)
|
||||
),
|
||||
suggested_values,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="set_options",
|
||||
data_schema=schema,
|
||||
errors=errors or None,
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
async def _get_location_data(self) -> dict[str, str]:
|
||||
@@ -428,3 +304,77 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
_LOGGER.debug("Location data: %s", location_data)
|
||||
|
||||
return location_data
|
||||
|
||||
async_step_user = async_step_set_options
|
||||
async_step_reconfigure = async_step_set_options
|
||||
|
||||
|
||||
def anthropic_config_option_schema(
|
||||
hass: HomeAssistant,
|
||||
is_new: bool,
|
||||
options: Mapping[str, Any],
|
||||
) -> dict:
|
||||
"""Return a schema for Anthropic completion options."""
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
SelectOptionDict(
|
||||
label=api.name,
|
||||
value=api.id,
|
||||
)
|
||||
for api in llm.async_get_apis(hass)
|
||||
]
|
||||
|
||||
if is_new:
|
||||
schema: dict[vol.Required | vol.Optional, Any] = {
|
||||
vol.Required(CONF_NAME, default=DEFAULT_CONVERSATION_NAME): str,
|
||||
}
|
||||
else:
|
||||
schema = {}
|
||||
|
||||
schema.update(
|
||||
{
|
||||
vol.Optional(CONF_PROMPT): TemplateSelector(),
|
||||
vol.Optional(
|
||||
CONF_LLM_HASS_API,
|
||||
): SelectSelector(SelectSelectorConfig(options=hass_apis, multiple=True)),
|
||||
vol.Required(
|
||||
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
|
||||
if options.get(CONF_RECOMMENDED):
|
||||
return schema
|
||||
|
||||
schema.update(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_CHAT_MODEL,
|
||||
default=RECOMMENDED_CHAT_MODEL,
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=RECOMMENDED_MAX_TOKENS,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=RECOMMENDED_TEMPERATURE,
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
||||
vol.Optional(
|
||||
CONF_THINKING_BUDGET,
|
||||
default=RECOMMENDED_THINKING_BUDGET,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH,
|
||||
default=RECOMMENDED_WEB_SEARCH,
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_MAX_USES,
|
||||
default=RECOMMENDED_WEB_SEARCH_MAX_USES,
|
||||
): int,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
default=RECOMMENDED_WEB_SEARCH_USER_LOCATION,
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
return schema
|
||||
|
||||
@@ -24,44 +24,37 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"entry_type": "Conversation agent",
|
||||
|
||||
"error": {
|
||||
"thinking_budget_too_large": "Maximum tokens must be greater than the thinking budget.",
|
||||
"web_search_unsupported_model": "Web search is not supported by the selected model. Please choose a compatible model or disable web search."
|
||||
},
|
||||
"initiate_flow": {
|
||||
"reconfigure": "Reconfigure conversation agent",
|
||||
"user": "Add conversation agent"
|
||||
},
|
||||
"step": {
|
||||
"advanced": {
|
||||
"set_options": {
|
||||
"data": {
|
||||
"chat_model": "[%key:common::generic::model%]",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"temperature": "Temperature"
|
||||
},
|
||||
"title": "Advanced settings"
|
||||
},
|
||||
"init": {
|
||||
"data": {
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
|
||||
"max_tokens": "Maximum tokens to return in response",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"prompt": "[%key:common::config_flow::data::prompt%]",
|
||||
"recommended": "Recommended model settings"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template."
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"data": {
|
||||
"recommended": "Recommended model settings",
|
||||
"temperature": "Temperature",
|
||||
"thinking_budget": "Thinking budget",
|
||||
"user_location": "Include home location",
|
||||
"web_search": "Enable web search",
|
||||
"web_search_max_uses": "Maximum web searches"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"thinking_budget": "The number of tokens the model can use to think about the response out of the total maximum number of tokens. Set to 1024 or greater to enable extended thinking.",
|
||||
"user_location": "Localize search results based on home location",
|
||||
"web_search": "The web search tool gives Claude direct access to real-time web content, allowing it to answer questions with up-to-date information beyond its knowledge cutoff",
|
||||
"web_search_max_uses": "Limit the number of searches performed per response"
|
||||
},
|
||||
"title": "Model-specific options"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
from homeassistant.helpers import (
|
||||
config_entry_oauth2_flow,
|
||||
device_registry as dr,
|
||||
issue_registry as ir,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_AUGUST_BRAND, DOMAIN, PLATFORMS
|
||||
@@ -38,10 +37,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
|
||||
|
||||
session = async_create_august_clientsession(hass)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ConfigEntryNotReady("OAuth implementation not available") from err
|
||||
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_august(hass, entry, august_gateway)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["avea"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["avea==1.6.1"]
|
||||
"requirements": ["avea==1.5.1"]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bluetooth-adapters==2.1.0",
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.28.4",
|
||||
"dbus-fast==2.45.0",
|
||||
"dbus-fast==2.44.5",
|
||||
"habluetooth==5.7.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==2.1.0", "icalendar==6.3.1", "vobject==0.9.9"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@ async def _async_reproduce_states(
|
||||
await call_service(SERVICE_SET_HVAC_MODE, [], {ATTR_HVAC_MODE: state.state})
|
||||
|
||||
if (
|
||||
(state.attributes.get(ATTR_TEMPERATURE) is not None)
|
||||
or (state.attributes.get(ATTR_TARGET_TEMP_HIGH) is not None)
|
||||
or (state.attributes.get(ATTR_TARGET_TEMP_LOW) is not None)
|
||||
(ATTR_TEMPERATURE in state.attributes)
|
||||
or (ATTR_TARGET_TEMP_HIGH in state.attributes)
|
||||
or (ATTR_TARGET_TEMP_LOW in state.attributes)
|
||||
):
|
||||
await call_service(
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
|
||||
@@ -14,9 +14,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ObjectClassType
|
||||
from .coordinator import ComelitConfigEntry, ComelitVedoSystem
|
||||
from .utils import new_device_listener
|
||||
from .utils import DeviceType, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -31,7 +30,7 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
|
||||
|
||||
@@ -2,20 +2,10 @@
|
||||
|
||||
import logging
|
||||
|
||||
from aiocomelit.api import (
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoAreaObject,
|
||||
ComelitVedoZoneObject,
|
||||
)
|
||||
from aiocomelit.const import BRIDGE, VEDO
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ObjectClassType = (
|
||||
ComelitSerialBridgeObject | ComelitVedoAreaObject | ComelitVedoZoneObject
|
||||
)
|
||||
|
||||
|
||||
DOMAIN = "comelit"
|
||||
DEFAULT_PORT = 80
|
||||
DEVICE_TYPE_LIST = [BRIDGE, VEDO]
|
||||
|
||||
@@ -10,6 +10,8 @@ from aiocomelit.api import (
|
||||
ComeliteSerialBridgeApi,
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoApi,
|
||||
ComelitVedoAreaObject,
|
||||
ComelitVedoZoneObject,
|
||||
)
|
||||
from aiocomelit.const import (
|
||||
BRIDGE,
|
||||
@@ -30,7 +32,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, DOMAIN, SCAN_INTERVAL, ObjectClassType
|
||||
from .const import _LOGGER, DOMAIN, SCAN_INTERVAL
|
||||
|
||||
type ComelitConfigEntry = ConfigEntry[ComelitBaseCoordinator]
|
||||
|
||||
@@ -75,7 +77,9 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
||||
|
||||
def platform_device_info(
|
||||
self,
|
||||
object_class: ObjectClassType,
|
||||
object_class: ComelitVedoZoneObject
|
||||
| ComelitVedoAreaObject
|
||||
| ComelitSerialBridgeObject,
|
||||
object_type: str,
|
||||
) -> dr.DeviceInfo:
|
||||
"""Set platform device info."""
|
||||
|
||||
@@ -12,10 +12,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import ObjectClassType
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import bridge_api_call, new_device_listener
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -30,7 +29,7 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
|
||||
|
||||
@@ -10,10 +10,9 @@ from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ObjectClassType
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import bridge_api_call, new_device_listener
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -28,7 +27,7 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitLightEntity(coordinator, device, config_entry.entry_id)
|
||||
|
||||
@@ -18,10 +18,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ObjectClassType
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge, ComelitVedoSystem
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import new_device_listener
|
||||
from .utils import DeviceType, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -67,7 +66,7 @@ async def async_setup_bridge_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitBridgeSensorEntity(
|
||||
@@ -94,7 +93,7 @@ async def async_setup_vedo_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitVedoSensorEntity(
|
||||
|
||||
@@ -11,10 +11,9 @@ from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ObjectClassType
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import bridge_api_call, new_device_listener
|
||||
from .utils import DeviceType, bridge_api_call, new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -29,7 +28,7 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
def _add_new_entities(new_devices: list[ObjectClassType], dev_type: str) -> None:
|
||||
def _add_new_entities(new_devices: list[DeviceType], dev_type: str) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiocomelit.api import ComelitSerialBridgeObject
|
||||
from aiocomelit.api import (
|
||||
ComelitSerialBridgeObject,
|
||||
ComelitVedoAreaObject,
|
||||
ComelitVedoZoneObject,
|
||||
)
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
@@ -18,10 +22,12 @@ from homeassistant.helpers import (
|
||||
entity_registry as er,
|
||||
)
|
||||
|
||||
from .const import _LOGGER, DOMAIN, ObjectClassType
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .coordinator import ComelitBaseCoordinator
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
|
||||
DeviceType = ComelitSerialBridgeObject | ComelitVedoAreaObject | ComelitVedoZoneObject
|
||||
|
||||
|
||||
async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
"""Return a new aiohttp session."""
|
||||
@@ -120,7 +126,11 @@ def new_device_listener(
|
||||
coordinator: ComelitBaseCoordinator,
|
||||
new_devices_callback: Callable[
|
||||
[
|
||||
list[ObjectClassType],
|
||||
list[
|
||||
ComelitSerialBridgeObject
|
||||
| ComelitVedoAreaObject
|
||||
| ComelitVedoZoneObject
|
||||
],
|
||||
str,
|
||||
],
|
||||
None,
|
||||
@@ -132,10 +142,10 @@ def new_device_listener(
|
||||
|
||||
def _check_devices() -> None:
|
||||
"""Check for new devices and call callback with any new monitors."""
|
||||
if TYPE_CHECKING:
|
||||
assert coordinator.data
|
||||
if not coordinator.data:
|
||||
return
|
||||
|
||||
new_devices: list[ObjectClassType] = []
|
||||
new_devices: list[DeviceType] = []
|
||||
for _id in coordinator.data[data_type]:
|
||||
if _id not in (id_list := known_devices.get(data_type, [])):
|
||||
known_devices.update({data_type: [*id_list, _id]})
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/elkm1",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["elkm1_lib"],
|
||||
"requirements": ["elkm1-lib==2.2.12"]
|
||||
"requirements": ["elkm1-lib==2.2.11"]
|
||||
}
|
||||
|
||||
@@ -189,7 +189,9 @@ class ElkPanel(ElkSensor):
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
if self._elk.is_connected():
|
||||
self._attr_native_value = "Paused" if self._elk.is_paused() else "Connected"
|
||||
self._attr_native_value = (
|
||||
"Paused" if self._element.remote_programming_status else "Connected"
|
||||
)
|
||||
else:
|
||||
self._attr_native_value = "Disconnected"
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
FlowType,
|
||||
OptionsFlowWithReload,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
@@ -918,7 +918,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle a option flow for esphome."""
|
||||
|
||||
async def async_step_init(
|
||||
|
||||
@@ -442,6 +442,14 @@ class RuntimeEntryData:
|
||||
# save delay has passed.
|
||||
await self.store.async_save(self._pending_storage())
|
||||
|
||||
async def async_update_listener(
|
||||
self, hass: HomeAssistant, entry: ESPHomeConfigEntry
|
||||
) -> None:
|
||||
"""Handle options update."""
|
||||
if self.original_options == entry.options:
|
||||
return
|
||||
hass.async_create_task(hass.config_entries.async_reload(entry.entry_id))
|
||||
|
||||
@callback
|
||||
def async_on_disconnect(self) -> None:
|
||||
"""Call when the entry has been disconnected.
|
||||
|
||||
@@ -983,6 +983,10 @@ class ESPHomeManager:
|
||||
|
||||
await reconnect_logic.start()
|
||||
|
||||
entry.async_on_unload(
|
||||
entry.add_update_listener(entry_data.async_update_listener)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_setup_device_registry(
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==42.7.0",
|
||||
"aioesphomeapi==42.6.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.4.0"
|
||||
],
|
||||
|
||||
@@ -460,27 +460,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
hass.http.app.router.register_resource(IndexView(repo_path, hass))
|
||||
|
||||
async_register_built_in_panel(
|
||||
hass,
|
||||
"light",
|
||||
sidebar_icon="mdi:lamps",
|
||||
sidebar_title="light",
|
||||
sidebar_default_visible=False,
|
||||
)
|
||||
async_register_built_in_panel(
|
||||
hass,
|
||||
"security",
|
||||
sidebar_icon="mdi:security",
|
||||
sidebar_title="security",
|
||||
sidebar_default_visible=False,
|
||||
)
|
||||
async_register_built_in_panel(
|
||||
hass,
|
||||
"climate",
|
||||
sidebar_icon="mdi:home-thermometer",
|
||||
sidebar_title="climate",
|
||||
sidebar_default_visible=False,
|
||||
)
|
||||
async_register_built_in_panel(hass, "light")
|
||||
async_register_built_in_panel(hass, "security")
|
||||
async_register_built_in_panel(hass, "climate")
|
||||
|
||||
async_register_built_in_panel(hass, "profile")
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
DATA_SUPERVISOR_STATS = "hassio_supervisor_stats"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
DATA_MOUNTS_INFO = "hassio_mounts_info"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
|
||||
@@ -10,7 +10,11 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohasupervisor import SupervisorError, SupervisorNotFoundError
|
||||
from aiohasupervisor.models import StoreInfo
|
||||
from aiohasupervisor.models.mounts import CIFSMountResponse, NFSMountResponse
|
||||
from aiohasupervisor.models.mounts import (
|
||||
CIFSMountResponse,
|
||||
MountsInfo,
|
||||
NFSMountResponse,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_MANUFACTURER, ATTR_NAME
|
||||
@@ -46,6 +50,7 @@ from .const import (
|
||||
DATA_KEY_OS,
|
||||
DATA_KEY_SUPERVISOR,
|
||||
DATA_KEY_SUPERVISOR_ISSUES,
|
||||
DATA_MOUNTS_INFO,
|
||||
DATA_NETWORK_INFO,
|
||||
DATA_OS_INFO,
|
||||
DATA_STORE,
|
||||
@@ -176,6 +181,16 @@ def get_core_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
return hass.data.get(DATA_CORE_INFO)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_mounts_info(hass: HomeAssistant) -> MountsInfo | None:
|
||||
"""Return Home Assistant mounts information from Supervisor.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data.get(DATA_MOUNTS_INFO)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_issues_info(hass: HomeAssistant) -> SupervisorIssues | None:
|
||||
@@ -349,7 +364,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
addons_info = get_addons_info(self.hass) or {}
|
||||
addons_stats = get_addons_stats(self.hass)
|
||||
store_data = get_store(self.hass)
|
||||
mounts_info = await self.supervisor_client.mounts.info()
|
||||
|
||||
if store_data:
|
||||
repositories = {
|
||||
@@ -384,7 +398,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
**get_supervisor_stats(self.hass),
|
||||
}
|
||||
new_data[DATA_KEY_HOST] = get_host_info(self.hass) or {}
|
||||
new_data[DATA_KEY_MOUNTS] = {mount.name: mount for mount in mounts_info.mounts}
|
||||
new_data[DATA_KEY_MOUNTS] = {
|
||||
mount.name: mount
|
||||
for mount in getattr(get_mounts_info(self.hass), "mounts", [])
|
||||
}
|
||||
|
||||
# If this is the initial refresh, register all addons and return the dict
|
||||
if is_first_update:
|
||||
@@ -468,6 +485,7 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
DATA_CORE_INFO: hassio.get_core_info(),
|
||||
DATA_SUPERVISOR_INFO: hassio.get_supervisor_info(),
|
||||
DATA_OS_INFO: hassio.get_os_info(),
|
||||
DATA_MOUNTS_INFO: self.supervisor_client.mounts.info(),
|
||||
}
|
||||
if CONTAINER_STATS in container_updates[CORE_CONTAINER]:
|
||||
updates[DATA_CORE_STATS] = hassio.get_core_stats()
|
||||
|
||||
@@ -15,7 +15,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, device_registry as dr
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import DOMAIN, PLATFORM_LOOKUP, PLATFORMS
|
||||
@@ -46,16 +47,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool
|
||||
except HiveReauthRequired as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, devices["parent"][0]["device_id"])},
|
||||
name=devices["parent"][0]["hiveName"],
|
||||
model=devices["parent"][0]["deviceData"]["model"],
|
||||
sw_version=devices["parent"][0]["deviceData"]["version"],
|
||||
manufacturer=devices["parent"][0]["deviceData"]["manufacturer"],
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry,
|
||||
[
|
||||
@@ -83,7 +74,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> Non
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: HiveConfigEntry, device_entry: dr.DeviceEntry
|
||||
hass: HomeAssistant, config_entry: HiveConfigEntry, device_entry: DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove a config entry from a device."""
|
||||
return True
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["apyhiveapi"],
|
||||
"requirements": ["pyhive-integration==1.0.6"]
|
||||
"requirements": ["pyhive-integration==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -12,11 +12,10 @@ import jwt
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
from homeassistant.helpers import (
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -49,15 +48,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HomeConnectConfigEntry) -> bool:
|
||||
"""Set up Home Connect from a config entry."""
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
|
||||
config_entry_auth = AsyncConfigEntryAuth(hass, session)
|
||||
try:
|
||||
|
||||
@@ -1236,9 +1236,6 @@
|
||||
"fetch_api_error": {
|
||||
"message": "Error obtaining data from the API: {error}"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation temporarily unavailable, will retry"
|
||||
},
|
||||
"pause_program": {
|
||||
"message": "Error pausing program: {error}"
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.4.0"]
|
||||
"requirements": ["homematicip==2.3.1"]
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class ClearTrafficStatisticsButton(BaseButton):
|
||||
|
||||
entity_description = ButtonEntityDescription(
|
||||
key=BUTTON_KEY_CLEAR_TRAFFIC_STATISTICS,
|
||||
translation_key="clear_traffic_statistics",
|
||||
name="Clear traffic statistics",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
@@ -87,6 +87,7 @@ class RestartButton(BaseButton):
|
||||
|
||||
entity_description = ButtonEntityDescription(
|
||||
key=BUTTON_KEY_RESTART,
|
||||
name="Restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
@@ -128,9 +128,6 @@
|
||||
"sms_messages_sim": {
|
||||
"default": "mdi:email-arrow-left"
|
||||
},
|
||||
"sms_new": {
|
||||
"default": "mdi:email-arrow-left"
|
||||
},
|
||||
"sms_outbox_device": {
|
||||
"default": "mdi:email-arrow-right"
|
||||
},
|
||||
|
||||
@@ -61,7 +61,9 @@ rules:
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
entity-translations:
|
||||
status: todo
|
||||
comment: Buttons and selects are lacking translations.
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: done
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
|
||||
from . import Router
|
||||
from .const import DOMAIN, KEY_NET_NET_MODE
|
||||
@@ -46,6 +47,7 @@ async def async_setup_entry(
|
||||
desc = HuaweiSelectEntityDescription(
|
||||
key=KEY_NET_NET_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
name="Preferred network mode",
|
||||
translation_key="preferred_network_mode",
|
||||
options=[
|
||||
NetworkModeEnum.MODE_AUTO.value,
|
||||
@@ -93,6 +95,11 @@ class HuaweiLteSelectEntity(HuaweiLteBaseEntityWithDevice, SelectEntity):
|
||||
self.key = key
|
||||
self.item = item
|
||||
|
||||
name = None
|
||||
if self.entity_description.name != UNDEFINED:
|
||||
name = self.entity_description.name
|
||||
self._attr_name = name or self.item
|
||||
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
self.entity_description.setter_fn(option)
|
||||
|
||||
@@ -501,12 +501,8 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
#
|
||||
KEY_MONITORING_CHECK_NOTIFICATIONS: HuaweiSensorGroup(
|
||||
exclude=re.compile(
|
||||
r"""^(
|
||||
OnlineUpdateStatus | # Could be useful, but what are the values?
|
||||
SimOperEvent | # Unknown
|
||||
SmsStorageFull # Handled by binary sensor
|
||||
)$""",
|
||||
re.IGNORECASE | re.VERBOSE,
|
||||
r"^(onlineupdatestatus|smsstoragefull)$",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
descriptions={
|
||||
"UnreadMessage": HuaweiSensorEntityDescription(
|
||||
@@ -628,20 +624,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"MaxDownloadRate": HuaweiSensorEntityDescription(
|
||||
key="MaxDownloadRate",
|
||||
translation_key="max_download_rate",
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"MaxUploadRate": HuaweiSensorEntityDescription(
|
||||
key="MaxUploadRate",
|
||||
translation_key="max_upload_rate",
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"TotalConnectTime": HuaweiSensorEntityDescription(
|
||||
key="TotalConnectTime",
|
||||
translation_key="total_connected_duration",
|
||||
@@ -727,10 +709,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
key="LocalUnread",
|
||||
translation_key="sms_unread_device",
|
||||
),
|
||||
"NewMsg": HuaweiSensorEntityDescription(
|
||||
key="NewMsg",
|
||||
translation_key="sms_new",
|
||||
),
|
||||
"SimDraft": HuaweiSensorEntityDescription(
|
||||
key="SimDraft",
|
||||
translation_key="sms_drafts_sim",
|
||||
@@ -776,25 +754,17 @@ async def async_setup_entry(
|
||||
items = filter(key_meta.include.search, items)
|
||||
if key_meta.exclude:
|
||||
items = [x for x in items if not key_meta.exclude.search(x)]
|
||||
for item in items:
|
||||
if not (desc := SENSOR_META[key].descriptions.get(item)):
|
||||
_LOGGER.debug( # pylint: disable=hass-logger-period # false positive
|
||||
(
|
||||
"Ignoring unknown sensor %s.%s. "
|
||||
"Opening an issue at GitHub against the "
|
||||
"huawei_lte integration would be appreciated, so we may be able to "
|
||||
"add support for it in a future release. "
|
||||
'Include the sensor name "%s.%s" in the issue, '
|
||||
"as well as any information you may have about it, "
|
||||
"such as values received for it as shown in the debug log."
|
||||
),
|
||||
key,
|
||||
item,
|
||||
key,
|
||||
item,
|
||||
)
|
||||
continue
|
||||
sensors.append(HuaweiLteSensor(router, key, item, desc))
|
||||
sensors.extend(
|
||||
HuaweiLteSensor(
|
||||
router,
|
||||
key,
|
||||
item,
|
||||
SENSOR_META[key].descriptions.get(
|
||||
item, HuaweiSensorEntityDescription(key=item)
|
||||
),
|
||||
)
|
||||
for item in items
|
||||
)
|
||||
|
||||
async_add_entities(sensors, True)
|
||||
|
||||
|
||||
@@ -53,10 +53,10 @@
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"5ghz_wifi_status": {
|
||||
"name": "Wi-Fi status (5GHz)"
|
||||
"name": "5GHz Wi-Fi status"
|
||||
},
|
||||
"24ghz_wifi_status": {
|
||||
"name": "Wi-Fi status (2.4GHz)"
|
||||
"name": "2.4GHz Wi-Fi status"
|
||||
},
|
||||
"mobile_connection": {
|
||||
"name": "Mobile connection"
|
||||
@@ -68,11 +68,6 @@
|
||||
"name": "Wi-Fi status"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"clear_traffic_statistics": {
|
||||
"name": "Clear traffic statistics"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"preferred_network_mode": {
|
||||
"name": "Preferred network mode",
|
||||
@@ -163,12 +158,6 @@
|
||||
"lte_uplink_frequency": {
|
||||
"name": "LTE uplink frequency"
|
||||
},
|
||||
"max_download_rate": {
|
||||
"name": "Maximum download rate"
|
||||
},
|
||||
"max_upload_rate": {
|
||||
"name": "Maximum upload rate"
|
||||
},
|
||||
"mode": {
|
||||
"name": "Mode"
|
||||
},
|
||||
@@ -308,9 +297,6 @@
|
||||
"sms_messages_sim": {
|
||||
"name": "SMS messages (SIM)"
|
||||
},
|
||||
"sms_new": {
|
||||
"name": "SMS new"
|
||||
},
|
||||
"sms_outbox_device": {
|
||||
"name": "SMS outbox (device)"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"loggers": ["xknx", "xknxproject"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"xknx==3.10.1",
|
||||
"xknx==3.10.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.10.31.195356"
|
||||
],
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -45,13 +44,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MieleConfigEntry) -> bool:
|
||||
"""Set up Miele from a config entry."""
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
|
||||
|
||||
@@ -1088,9 +1088,6 @@
|
||||
"invalid_target": {
|
||||
"message": "Invalid device targeted."
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation unavailable, will retry"
|
||||
},
|
||||
"set_program_error": {
|
||||
"message": "'Set program' action failed: {status} / {message}"
|
||||
},
|
||||
|
||||
@@ -239,7 +239,6 @@ class MotionBaseDevice(MotionCoordinatorEntity, CoverEntity):
|
||||
angle = kwargs.get(ATTR_TILT_POSITION)
|
||||
if angle is not None:
|
||||
angle = angle * 180 / 100
|
||||
angle = 180 - angle
|
||||
async with self._api_lock:
|
||||
await self.hass.async_add_executor_job(
|
||||
self._blind.Set_position,
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
"description": "Do you want to add the Music Assistant server `{url}` to Home Assistant?",
|
||||
"title": "Discovered Music Assistant server"
|
||||
},
|
||||
"user": {
|
||||
"init": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "URL of the Music Assistant server"
|
||||
}
|
||||
},
|
||||
"manual": {
|
||||
"data": {
|
||||
"url": "URL of the Music Assistant server"
|
||||
},
|
||||
"description": "Enter the URL to your already running Music Assistant server. If you do not have the Music Assistant server running, you should install it first.",
|
||||
"title": "Manually add Music Assistant server"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,12 +12,10 @@ from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
device_registry as dr,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
@@ -42,16 +40,13 @@ async def async_setup_entry(
|
||||
) -> bool:
|
||||
"""Set up myUplink from a config entry."""
|
||||
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, config_entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="implementation_unavailable",
|
||||
) from err
|
||||
|
||||
session = OAuth2Session(hass, config_entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, config_entry
|
||||
)
|
||||
)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
||||
|
||||
try:
|
||||
await auth.async_get_access_token()
|
||||
|
||||
@@ -56,9 +56,6 @@
|
||||
"config_entry_not_ready": {
|
||||
"message": "Error while loading the integration."
|
||||
},
|
||||
"implementation_unavailable": {
|
||||
"message": "OAuth2 implementation is not available, will retry."
|
||||
},
|
||||
"incorrect_oauth2_scope": {
|
||||
"message": "Stored permissions are invalid. Please login again to update permissions."
|
||||
},
|
||||
|
||||
@@ -22,7 +22,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -190,13 +189,7 @@ async def _get_onedrive_client(
|
||||
hass: HomeAssistant, entry: OneDriveConfigEntry
|
||||
) -> tuple[OneDriveClient, Callable[[], Awaitable[str]]]:
|
||||
"""Get OneDrive client."""
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
async def get_access_token() -> str:
|
||||
|
||||
@@ -108,9 +108,6 @@
|
||||
"no_access_to_path": {
|
||||
"message": "Cannot read {filename}, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation unavailable, will retry"
|
||||
},
|
||||
"update_failed": {
|
||||
"message": "Failed to update drive state"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aio_ownet"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aio-ownet==0.0.5"],
|
||||
"requirements": ["aio-ownet==0.0.4"],
|
||||
"zeroconf": ["_owserver._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"device_selection": "Customize specific devices"
|
||||
},
|
||||
"data_description": {
|
||||
"clear_device_options": "Use this to reset all device-specific options to default values.",
|
||||
"clear_device_options": "Use this to reset all device specific options to default values.",
|
||||
"device_selection": "Customize behavior of individual devices."
|
||||
},
|
||||
"description": "Select what configuration steps to process",
|
||||
|
||||
@@ -55,7 +55,6 @@ from .const import (
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
CONF_WEB_SEARCH_INLINE_CITATIONS,
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
CONF_WEB_SEARCH_TIMEZONE,
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
@@ -74,7 +73,6 @@ from .const import (
|
||||
RECOMMENDED_VERBOSITY,
|
||||
RECOMMENDED_WEB_SEARCH,
|
||||
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE,
|
||||
RECOMMENDED_WEB_SEARCH_INLINE_CITATIONS,
|
||||
RECOMMENDED_WEB_SEARCH_USER_LOCATION,
|
||||
UNSUPPORTED_IMAGE_MODELS,
|
||||
UNSUPPORTED_MODELS,
|
||||
@@ -398,10 +396,6 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
default=RECOMMENDED_WEB_SEARCH_USER_LOCATION,
|
||||
): bool,
|
||||
vol.Optional(
|
||||
CONF_WEB_SEARCH_INLINE_CITATIONS,
|
||||
default=RECOMMENDED_WEB_SEARCH_INLINE_CITATIONS,
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
elif CONF_WEB_SEARCH in options:
|
||||
@@ -417,7 +411,6 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
CONF_WEB_SEARCH_TIMEZONE,
|
||||
CONF_WEB_SEARCH_INLINE_CITATIONS,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ CONF_WEB_SEARCH_CITY = "city"
|
||||
CONF_WEB_SEARCH_REGION = "region"
|
||||
CONF_WEB_SEARCH_COUNTRY = "country"
|
||||
CONF_WEB_SEARCH_TIMEZONE = "timezone"
|
||||
CONF_WEB_SEARCH_INLINE_CITATIONS = "inline_citations"
|
||||
RECOMMENDED_CODE_INTERPRETER = False
|
||||
RECOMMENDED_CHAT_MODEL = "gpt-4o-mini"
|
||||
RECOMMENDED_IMAGE_MODEL = "gpt-image-1"
|
||||
@@ -42,7 +41,6 @@ RECOMMENDED_VERBOSITY = "medium"
|
||||
RECOMMENDED_WEB_SEARCH = False
|
||||
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE = "medium"
|
||||
RECOMMENDED_WEB_SEARCH_USER_LOCATION = False
|
||||
RECOMMENDED_WEB_SEARCH_INLINE_CITATIONS = False
|
||||
|
||||
UNSUPPORTED_MODELS: list[str] = [
|
||||
"o1-mini",
|
||||
|
||||
@@ -7,7 +7,6 @@ from collections.abc import AsyncGenerator, Callable, Iterable
|
||||
import json
|
||||
from mimetypes import guess_file_type
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
import openai
|
||||
@@ -30,7 +29,6 @@ from openai.types.responses import (
|
||||
ResponseInputImageParam,
|
||||
ResponseInputMessageContentListParam,
|
||||
ResponseInputParam,
|
||||
ResponseInputTextParam,
|
||||
ResponseOutputItemAddedEvent,
|
||||
ResponseOutputItemDoneEvent,
|
||||
ResponseOutputMessage,
|
||||
@@ -79,7 +77,6 @@ from .const import (
|
||||
CONF_WEB_SEARCH_CITY,
|
||||
CONF_WEB_SEARCH_CONTEXT_SIZE,
|
||||
CONF_WEB_SEARCH_COUNTRY,
|
||||
CONF_WEB_SEARCH_INLINE_CITATIONS,
|
||||
CONF_WEB_SEARCH_REGION,
|
||||
CONF_WEB_SEARCH_TIMEZONE,
|
||||
CONF_WEB_SEARCH_USER_LOCATION,
|
||||
@@ -93,7 +90,6 @@ from .const import (
|
||||
RECOMMENDED_TOP_P,
|
||||
RECOMMENDED_VERBOSITY,
|
||||
RECOMMENDED_WEB_SEARCH_CONTEXT_SIZE,
|
||||
RECOMMENDED_WEB_SEARCH_INLINE_CITATIONS,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -255,7 +251,6 @@ def _convert_content_to_param(
|
||||
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
|
||||
chat_log: conversation.ChatLog,
|
||||
stream: AsyncStream[ResponseStreamEvent],
|
||||
remove_citations: bool = False,
|
||||
) -> AsyncGenerator[
|
||||
conversation.AssistantContentDeltaDict | conversation.ToolResultContentDeltaDict
|
||||
]:
|
||||
@@ -263,13 +258,6 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
||||
last_summary_index = None
|
||||
last_role: Literal["assistant", "tool_result"] | None = None
|
||||
|
||||
# Non-reasoning models don't follow our request to remove citations, so we remove
|
||||
# them manually here. They always follow the same pattern: the citation is always
|
||||
# in parentheses in Markdown format, the citation is always in a single delta event,
|
||||
# and sometimes the closing parenthesis is split into a separate delta event.
|
||||
remove_parentheses: bool = False
|
||||
citation_regexp = re.compile(r"\(\[([^\]]+)\]\((https?:\/\/[^\)]+)\)")
|
||||
|
||||
async for event in stream:
|
||||
LOGGER.debug("Received event: %s", event)
|
||||
|
||||
@@ -356,23 +344,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
|
||||
yield {"native": event.item}
|
||||
last_summary_index = -1 # Trigger new assistant message on next turn
|
||||
elif isinstance(event, ResponseTextDeltaEvent):
|
||||
data = event.delta
|
||||
if remove_parentheses:
|
||||
data = data.removeprefix(")")
|
||||
remove_parentheses = False
|
||||
elif remove_citations and (match := citation_regexp.search(data)):
|
||||
match_start, match_end = match.span()
|
||||
# remove leading space if any
|
||||
if data[match_start - 1 : match_start] == " ":
|
||||
match_start -= 1
|
||||
# remove closing parenthesis:
|
||||
if data[match_end : match_end + 1] == ")":
|
||||
match_end += 1
|
||||
else:
|
||||
remove_parentheses = True
|
||||
data = data[:match_start] + data[match_end:]
|
||||
if data:
|
||||
yield {"content": data}
|
||||
yield {"content": event.delta}
|
||||
elif isinstance(event, ResponseReasoningSummaryTextDeltaEvent):
|
||||
# OpenAI can output several reasoning summaries
|
||||
# in a single ResponseReasoningItem. We split them as separate
|
||||
@@ -517,7 +489,6 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
for tool in chat_log.llm_api.tools
|
||||
]
|
||||
|
||||
remove_citations = False
|
||||
if options.get(CONF_WEB_SEARCH):
|
||||
web_search = WebSearchToolParam(
|
||||
type="web_search",
|
||||
@@ -533,27 +504,6 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
country=options.get(CONF_WEB_SEARCH_COUNTRY, ""),
|
||||
timezone=options.get(CONF_WEB_SEARCH_TIMEZONE, ""),
|
||||
)
|
||||
if not options.get(
|
||||
CONF_WEB_SEARCH_INLINE_CITATIONS,
|
||||
RECOMMENDED_WEB_SEARCH_INLINE_CITATIONS,
|
||||
):
|
||||
system_message = cast(EasyInputMessageParam, messages[0])
|
||||
content = system_message["content"]
|
||||
if isinstance(content, str):
|
||||
system_message["content"] = [
|
||||
ResponseInputTextParam(type="input_text", text=content)
|
||||
]
|
||||
system_message["content"].append( # type: ignore[union-attr]
|
||||
ResponseInputTextParam(
|
||||
type="input_text",
|
||||
text="When doing a web search, do not include source citations",
|
||||
)
|
||||
)
|
||||
|
||||
if "reasoning" not in model_args:
|
||||
# Reasoning models handle this correctly with just a prompt
|
||||
remove_citations = True
|
||||
|
||||
tools.append(web_search)
|
||||
|
||||
if options.get(CONF_CODE_INTERPRETER):
|
||||
@@ -623,8 +573,7 @@ class OpenAIBaseLLMEntity(Entity):
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
self.entity_id,
|
||||
_transform_stream(chat_log, stream, remove_citations),
|
||||
self.entity_id, _transform_stream(chat_log, stream)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"data": {
|
||||
"code_interpreter": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::code_interpreter%]",
|
||||
"image_model": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::image_model%]",
|
||||
"inline_citations": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::inline_citations%]",
|
||||
"reasoning_effort": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::reasoning_effort%]",
|
||||
"search_context_size": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::search_context_size%]",
|
||||
"user_location": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data::user_location%]",
|
||||
@@ -60,7 +59,6 @@
|
||||
"data_description": {
|
||||
"code_interpreter": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::code_interpreter%]",
|
||||
"image_model": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::image_model%]",
|
||||
"inline_citations": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::inline_citations%]",
|
||||
"reasoning_effort": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::reasoning_effort%]",
|
||||
"search_context_size": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::search_context_size%]",
|
||||
"user_location": "[%key:component::openai_conversation::config_subentries::conversation::step::model::data_description::user_location%]",
|
||||
@@ -76,6 +74,7 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"entry_type": "Conversation agent",
|
||||
|
||||
"error": {
|
||||
"model_not_supported": "This model is not supported, please select a different model",
|
||||
"web_search_minimal_reasoning": "Web search is currently not supported with minimal reasoning effort"
|
||||
@@ -109,7 +108,6 @@
|
||||
"data": {
|
||||
"code_interpreter": "Enable code interpreter tool",
|
||||
"image_model": "Image generation model",
|
||||
"inline_citations": "Include links in web search results",
|
||||
"reasoning_effort": "Reasoning effort",
|
||||
"search_context_size": "Search context size",
|
||||
"user_location": "Include home location",
|
||||
@@ -118,7 +116,6 @@
|
||||
"data_description": {
|
||||
"code_interpreter": "This tool, also known as the python tool to the model, allows it to run code to answer questions",
|
||||
"image_model": "The model to use when generating images",
|
||||
"inline_citations": "If disabled, additional prompt is added to ask the model to not include source citations",
|
||||
"reasoning_effort": "How many reasoning tokens the model should generate before creating a response to the prompt",
|
||||
"search_context_size": "High level guidance for the amount of context window space to use for the search",
|
||||
"user_location": "Refine search results based on geography",
|
||||
|
||||
@@ -47,19 +47,15 @@ OVERKIZ_TO_PRESET_MODE: dict[str, str] = {
|
||||
PRESET_MODE_TO_OVERKIZ = {v: k for k, v in OVERKIZ_TO_PRESET_MODE.items()}
|
||||
|
||||
# Map Overkiz HVAC modes to Home Assistant HVAC modes
|
||||
#
|
||||
# HVAC_MODE_TO_OVERKIZ reverses this mapping, thus order matters.
|
||||
# Multiple Overkiz states may map to the same Home Assistant HVAC mode,
|
||||
# reversing the mapping picks the last one.
|
||||
OVERKIZ_TO_HVAC_MODE: dict[str, HVACMode] = {
|
||||
OverkizCommandParam.ON: HVACMode.HEAT,
|
||||
OverkizCommandParam.MANUAL: HVACMode.HEAT,
|
||||
OverkizCommandParam.BASIC: HVACMode.HEAT, # main command
|
||||
OverkizCommandParam.OFF: HVACMode.OFF,
|
||||
OverkizCommandParam.STANDBY: HVACMode.OFF, # main command
|
||||
OverkizCommandParam.AUTO: HVACMode.AUTO,
|
||||
OverkizCommandParam.BASIC: HVACMode.HEAT,
|
||||
OverkizCommandParam.MANUAL: HVACMode.HEAT,
|
||||
OverkizCommandParam.STANDBY: HVACMode.OFF,
|
||||
OverkizCommandParam.EXTERNAL: HVACMode.AUTO,
|
||||
OverkizCommandParam.INTERNAL: HVACMode.AUTO, # main command
|
||||
OverkizCommandParam.INTERNAL: HVACMode.AUTO,
|
||||
}
|
||||
|
||||
OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = {
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .const import DEV_CLASS, DOMAIN, LOGGER, PLATFORMS
|
||||
from .const import DOMAIN, LOGGER, PLATFORMS
|
||||
from .coordinator import PlugwiseConfigEntry, PlugwiseDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: PlugwiseConfigEntry) ->
|
||||
def async_migrate_entity_entry(entry: er.RegistryEntry) -> dict[str, Any] | None:
|
||||
"""Migrate Plugwise entity entries.
|
||||
|
||||
Migrates old unique ID's from old binary_sensors and switches to the new unique ID's.
|
||||
- Migrates old unique ID's from old binary_sensors and switches to the new unique ID's
|
||||
"""
|
||||
if entry.domain == Platform.BINARY_SENSOR and entry.unique_id.endswith(
|
||||
"-slave_boiler_state"
|
||||
@@ -80,7 +80,7 @@ def migrate_sensor_entities(
|
||||
# Migrating opentherm_outdoor_temperature
|
||||
# to opentherm_outdoor_air_temperature sensor
|
||||
for device_id, device in coordinator.data.items():
|
||||
if device[DEV_CLASS] != "heater_central":
|
||||
if device["dev_class"] != "heater_central":
|
||||
continue
|
||||
|
||||
old_unique_id = f"{device_id}-outdoor_temperature"
|
||||
|
||||
@@ -31,18 +31,14 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import (
|
||||
ANNA_WITH_ADAM,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_USERNAME,
|
||||
DOMAIN,
|
||||
FLOW_SMILE,
|
||||
FLOW_STRETCH,
|
||||
SMILE,
|
||||
SMILE_OPEN_THERM,
|
||||
SMILE_THERMO,
|
||||
STRETCH,
|
||||
STRETCH_USERNAME,
|
||||
UNKNOWN_SMILE,
|
||||
ZEROCONF_MAP,
|
||||
)
|
||||
|
||||
@@ -121,7 +117,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
discovery_info: ZeroconfServiceInfo | None = None
|
||||
product: str = UNKNOWN_SMILE
|
||||
product: str = "Unknown Smile"
|
||||
_username: str = DEFAULT_USERNAME
|
||||
|
||||
async def async_step_zeroconf(
|
||||
@@ -155,20 +151,20 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if DEFAULT_USERNAME not in unique_id:
|
||||
self._username = STRETCH_USERNAME
|
||||
self.product = _product = _properties.get("product", UNKNOWN_SMILE)
|
||||
self.product = _product = _properties.get("product", "Unknown Smile")
|
||||
_version = _properties.get("version", "n/a")
|
||||
_name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}"
|
||||
|
||||
# This is an Anna, but we already have config entries.
|
||||
# Assuming that the user has already configured Adam, aborting discovery.
|
||||
if self._async_current_entries() and _product == SMILE_THERMO:
|
||||
return self.async_abort(reason=ANNA_WITH_ADAM)
|
||||
if self._async_current_entries() and _product == "smile_thermo":
|
||||
return self.async_abort(reason="anna_with_adam")
|
||||
|
||||
# If we have discovered an Adam or Anna, both might be on the network.
|
||||
# In that case, we need to cancel the Anna flow, as the Adam should
|
||||
# be added.
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
return self.async_abort(reason=ANNA_WITH_ADAM)
|
||||
return self.async_abort(reason="anna_with_adam")
|
||||
|
||||
self.context.update(
|
||||
{
|
||||
@@ -183,11 +179,11 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
# This is an Anna, and there is already an Adam flow in progress
|
||||
if self.product == SMILE_THERMO and other_flow.product == SMILE_OPEN_THERM:
|
||||
if self.product == "smile_thermo" and other_flow.product == "smile_open_therm":
|
||||
return True
|
||||
|
||||
# This is an Adam, and there is already an Anna flow in progress
|
||||
if self.product == SMILE_OPEN_THERM and other_flow.product == SMILE_THERMO:
|
||||
if self.product == "smile_open_therm" and other_flow.product == "smile_thermo":
|
||||
self.hass.config_entries.flow.async_abort(other_flow.flow_id)
|
||||
|
||||
return False
|
||||
|
||||
@@ -12,10 +12,7 @@ DOMAIN: Final = "plugwise"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ANNA_WITH_ADAM: Final = "anna_with_adam"
|
||||
API: Final = "api"
|
||||
AVAILABLE: Final = "available"
|
||||
DEV_CLASS: Final = "dev_class"
|
||||
FLOW_SMILE: Final = "smile (Adam/Anna/P1)"
|
||||
FLOW_STRETCH: Final = "stretch (Stretch)"
|
||||
FLOW_TYPE: Final = "flow_type"
|
||||
@@ -24,11 +21,8 @@ LOCATION: Final = "location"
|
||||
PW_TYPE: Final = "plugwise_type"
|
||||
REBOOT: Final = "reboot"
|
||||
SMILE: Final = "smile"
|
||||
SMILE_OPEN_THERM: Final = "smile_open_therm"
|
||||
SMILE_THERMO: Final = "smile_thermo"
|
||||
STRETCH: Final = "stretch"
|
||||
STRETCH_USERNAME: Final = "stretch"
|
||||
UNKNOWN_SMILE: Final = "Unknown Smile"
|
||||
|
||||
PLATFORMS: Final[list[str]] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import (
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import AVAILABLE, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PlugwiseDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]):
|
||||
if device_id != coordinator.api.gateway_id:
|
||||
self._attr_device_info.update(
|
||||
{
|
||||
ATTR_NAME: data.get(ATTR_NAME),
|
||||
ATTR_NAME: data.get("name"),
|
||||
ATTR_VIA_DEVICE: (
|
||||
DOMAIN,
|
||||
str(self.coordinator.api.gateway_id),
|
||||
@@ -69,7 +69,7 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]):
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self._dev_id in self.coordinator.data
|
||||
and (AVAILABLE not in self.device or self.device[AVAILABLE] is True)
|
||||
and ("available" not in self.device or self.device["available"] is True)
|
||||
and super().available
|
||||
)
|
||||
|
||||
|
||||
@@ -11,12 +11,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_WEBHOOK_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from . import api
|
||||
@@ -36,14 +31,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: PointConfigEntry) -> boo
|
||||
if "auth_implementation" not in entry.data:
|
||||
raise ConfigEntryAuthFailed("Authentication failed. Please re-authenticate.")
|
||||
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
auth = api.AsyncConfigEntryAuth(
|
||||
aiohttp_client.async_get_clientsession(hass), session
|
||||
)
|
||||
|
||||
@@ -33,10 +33,5 @@
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation unavailable, will retry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = (
|
||||
translation_key="containers_count",
|
||||
value_fn=lambda data: data.docker_info.containers,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PortainerEndpointSensorEntityDescription(
|
||||
@@ -168,6 +169,7 @@ ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = (
|
||||
translation_key="containers_running",
|
||||
value_fn=lambda data: data.docker_info.containers_running,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PortainerEndpointSensorEntityDescription(
|
||||
@@ -175,6 +177,7 @@ ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = (
|
||||
translation_key="containers_stopped",
|
||||
value_fn=lambda data: data.docker_info.containers_stopped,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PortainerEndpointSensorEntityDescription(
|
||||
@@ -182,6 +185,7 @@ ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = (
|
||||
translation_key="containers_paused",
|
||||
value_fn=lambda data: data.docker_info.containers_paused,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PortainerEndpointSensorEntityDescription(
|
||||
@@ -189,6 +193,7 @@ ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = (
|
||||
translation_key="images_count",
|
||||
value_fn=lambda data: data.docker_info.images,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
PortainerEndpointSensorEntityDescription(
|
||||
@@ -198,7 +203,6 @@ ENDPOINT_SENSORS: tuple[PortainerEndpointSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.MEBIBYTES,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==3.7.1",
|
||||
"python-roborock==3.7.0",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,25 +2,24 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from satel_integra.satel_integra import AsyncSatel
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import CONF_CODE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_SWITCHABLE_OUTPUT_NUMBER,
|
||||
DOMAIN,
|
||||
SIGNAL_OUTPUTS_UPDATED,
|
||||
SUBENTRY_TYPE_SWITCHABLE_OUTPUT,
|
||||
SatelConfigEntry,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -37,8 +36,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
for subentry in switchable_output_subentries:
|
||||
switchable_output_num: int = subentry.data[CONF_SWITCHABLE_OUTPUT_NUMBER]
|
||||
switchable_output_name: str = subentry.data[CONF_NAME]
|
||||
switchable_output_num = subentry.data[CONF_SWITCHABLE_OUTPUT_NUMBER]
|
||||
switchable_output_name = subentry.data[CONF_NAME]
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
@@ -58,54 +57,58 @@ class SatelIntegraSwitch(SwitchEntity):
|
||||
"""Representation of an Satel switch."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
controller: AsyncSatel,
|
||||
device_number: int,
|
||||
device_name: str,
|
||||
code: str | None,
|
||||
config_entry_id: str,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
def __init__(self, controller, device_number, device_name, code, config_entry_id):
|
||||
"""Initialize the binary_sensor."""
|
||||
self._device_number = device_number
|
||||
self._attr_unique_id = f"{config_entry_id}_switch_{device_number}"
|
||||
self._name = device_name
|
||||
self._state = False
|
||||
self._code = code
|
||||
self._satel = controller
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=device_name, identifiers={(DOMAIN, self._attr_unique_id)}
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self._attr_is_on = self._device_number in self._satel.violated_outputs
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_OUTPUTS_UPDATED, self._devices_updated
|
||||
)
|
||||
async_dispatcher_connect(
|
||||
self.hass, SIGNAL_OUTPUTS_UPDATED, self._devices_updated
|
||||
)
|
||||
|
||||
@callback
|
||||
def _devices_updated(self, outputs: dict[int, int]) -> None:
|
||||
def _devices_updated(self, zones):
|
||||
"""Update switch state, if needed."""
|
||||
if self._device_number in outputs:
|
||||
new_state = outputs[self._device_number] == 1
|
||||
if new_state != self._attr_is_on:
|
||||
self._attr_is_on = new_state
|
||||
_LOGGER.debug("Update switch name: %s zones: %s", self._name, zones)
|
||||
if self._device_number in zones:
|
||||
new_state = self._read_state()
|
||||
_LOGGER.debug("New state: %s", new_state)
|
||||
if new_state != self._state:
|
||||
self._state = new_state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
_LOGGER.debug("Switch: %s status: %s, turning on", self._name, self._state)
|
||||
await self._satel.set_output(self._code, self._device_number, True)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
_LOGGER.debug(
|
||||
"Switch name: %s status: %s, turning off", self._name, self._state
|
||||
)
|
||||
await self._satel.set_output(self._code, self._device_number, False)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Return true if device is on."""
|
||||
self._state = self._read_state()
|
||||
return self._state
|
||||
|
||||
def _read_state(self):
|
||||
"""Read state of the device."""
|
||||
return self._device_number in self._satel.violated_outputs
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the switch."""
|
||||
return self._name
|
||||
|
||||
@@ -21,7 +21,6 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
@@ -31,7 +30,6 @@ from .const import (
|
||||
MODEL_FRANKEVER_WATER_VALVE,
|
||||
ROLE_GENERIC,
|
||||
SHELLY_GAS_MODELS,
|
||||
SHELLY_WALL_DISPLAY_MODELS,
|
||||
)
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
@@ -64,7 +62,6 @@ class ShellyButtonDescription[
|
||||
"""Class to describe a Button entity."""
|
||||
|
||||
press_action: str
|
||||
params: dict[str, Any] | None = None
|
||||
|
||||
supported: Callable[[_ShellyCoordinatorT], bool] = lambda _: True
|
||||
|
||||
@@ -77,12 +74,14 @@ class RpcButtonDescription(RpcEntityDescription, ButtonEntityDescription):
|
||||
BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
ShellyButtonDescription[ShellyBlockCoordinator | ShellyRpcCoordinator](
|
||||
key="reboot",
|
||||
name="Restart",
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_reboot",
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="self_test",
|
||||
name="Self test",
|
||||
translation_key="self_test",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
press_action="trigger_shelly_gas_self_test",
|
||||
@@ -90,32 +89,20 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="mute",
|
||||
translation_key="mute_alarm",
|
||||
name="Mute",
|
||||
translation_key="mute",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_shelly_gas_mute",
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="unmute",
|
||||
translation_key="unmute_alarm",
|
||||
name="Unmute",
|
||||
translation_key="unmute",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_shelly_gas_unmute",
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
ShellyButtonDescription[ShellyRpcCoordinator](
|
||||
key="turn_on_screen",
|
||||
translation_key="turn_on_the_screen",
|
||||
press_action="wall_display_set_screen",
|
||||
params={"value": True},
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_WALL_DISPLAY_MODELS,
|
||||
),
|
||||
ShellyButtonDescription[ShellyRpcCoordinator](
|
||||
key="turn_off_screen",
|
||||
translation_key="turn_off_the_screen",
|
||||
press_action="wall_display_set_screen",
|
||||
params={"value": False},
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_WALL_DISPLAY_MODELS,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -330,7 +317,7 @@ class ShellyButton(ShellyBaseButton):
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
await method(**(self.entity_description.params or {}))
|
||||
await method()
|
||||
|
||||
|
||||
class ShellyBluTrvButton(ShellyRpcAttributeEntity, ButtonEntity):
|
||||
@@ -344,7 +331,7 @@ class ShellyBluTrvButton(ShellyRpcAttributeEntity, ButtonEntity):
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcButtonDescription,
|
||||
description: RpcEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize button."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
@@ -358,9 +345,6 @@ class ShellyBluTrvButton(ShellyRpcAttributeEntity, ButtonEntity):
|
||||
config, ble_addr, coordinator.mac, fw_ver
|
||||
)
|
||||
|
||||
if hasattr(self, "_attr_name") and description.role != ROLE_GENERIC:
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
@rpc_call
|
||||
async def async_press(self) -> None:
|
||||
"""Triggers the Shelly button press service."""
|
||||
@@ -373,19 +357,6 @@ class RpcVirtualButton(ShellyRpcAttributeEntity, ButtonEntity):
|
||||
entity_description: RpcButtonDescription
|
||||
_id: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcButtonDescription,
|
||||
) -> None:
|
||||
"""Initialize select."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
|
||||
if hasattr(self, "_attr_name") and description.role != ROLE_GENERIC:
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
@rpc_call
|
||||
async def async_press(self) -> None:
|
||||
"""Triggers the Shelly button press service."""
|
||||
@@ -400,20 +371,6 @@ class RpcSleepingSmokeMuteButton(ShellySleepingRpcAttributeEntity, ButtonEntity)
|
||||
|
||||
entity_description: RpcButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcButtonDescription,
|
||||
entry: RegistryEntry | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sleeping sensor."""
|
||||
super().__init__(coordinator, key, attribute, description, entry)
|
||||
|
||||
if hasattr(self, "_attr_name"):
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
@rpc_call
|
||||
async def async_press(self) -> None:
|
||||
"""Triggers the Shelly button press service."""
|
||||
@@ -441,20 +398,19 @@ RPC_BUTTONS = {
|
||||
),
|
||||
"button_open": RpcButtonDescription(
|
||||
key="button",
|
||||
translation_key="open",
|
||||
entity_registry_enabled_default=False,
|
||||
role="open",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
"button_close": RpcButtonDescription(
|
||||
key="button",
|
||||
translation_key="close",
|
||||
entity_registry_enabled_default=False,
|
||||
role="close",
|
||||
models={MODEL_FRANKEVER_WATER_VALVE},
|
||||
),
|
||||
"calibrate": RpcButtonDescription(
|
||||
key="blutrv",
|
||||
name="Calibrate",
|
||||
translation_key="calibrate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_class=ShellyBluTrvButton,
|
||||
@@ -463,6 +419,7 @@ RPC_BUTTONS = {
|
||||
"smoke_mute": RpcButtonDescription(
|
||||
key="smoke",
|
||||
sub_key="mute",
|
||||
translation_key="mute_alarm",
|
||||
name="Mute alarm",
|
||||
translation_key="mute",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -223,11 +223,6 @@ UPTIME_DEVIATION: Final = 60
|
||||
ENTRY_RELOAD_COOLDOWN = 60
|
||||
|
||||
SHELLY_GAS_MODELS = [MODEL_GAS]
|
||||
SHELLY_WALL_DISPLAY_MODELS = (
|
||||
MODEL_WALL_DISPLAY,
|
||||
MODEL_WALL_DISPLAY_X2,
|
||||
MODEL_WALL_DISPLAY_XL,
|
||||
)
|
||||
|
||||
CONF_BLE_SCANNER_MODE = "ble_scanner_mode"
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"mute_alarm": {
|
||||
"mute": {
|
||||
"default": "mdi:volume-mute"
|
||||
},
|
||||
"self_test": {
|
||||
"default": "mdi:progress-wrench"
|
||||
},
|
||||
"unmute_alarm": {
|
||||
"unmute": {
|
||||
"default": "mdi:volume-high"
|
||||
}
|
||||
},
|
||||
@@ -70,13 +70,6 @@
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"cury_away_mode": {
|
||||
"default": "mdi:home-outline",
|
||||
"state": {
|
||||
"off": "mdi:home-import-outline",
|
||||
"on": "mdi:home-export-outline"
|
||||
}
|
||||
},
|
||||
"cury_boost": {
|
||||
"default": "mdi:rocket-launch"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioshelly==13.17.0"],
|
||||
"requirements": ["aioshelly==13.16.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "shelly*",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Final
|
||||
from typing import Final
|
||||
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
|
||||
@@ -37,92 +37,14 @@ PARALLEL_UPDATES = 0
|
||||
class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription):
|
||||
"""Class to describe a RPC select entity."""
|
||||
|
||||
method: str
|
||||
|
||||
|
||||
class RpcSelect(ShellyRpcAttributeEntity, SelectEntity):
|
||||
"""Represent a RPC select entity."""
|
||||
|
||||
entity_description: RpcSelectDescription
|
||||
_id: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcSelectDescription,
|
||||
) -> None:
|
||||
"""Initialize select."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
|
||||
if self.option_map:
|
||||
self._attr_options = list(self.option_map.values())
|
||||
|
||||
if hasattr(self, "_attr_name") and description.role != ROLE_GENERIC:
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
if isinstance(self.attribute_value, str) and self.option_map:
|
||||
return self.option_map[self.attribute_value]
|
||||
|
||||
return None
|
||||
|
||||
@rpc_call
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the value."""
|
||||
method = getattr(self.coordinator.device, self.entity_description.method)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
if self.reversed_option_map:
|
||||
await method(self._id, self.reversed_option_map[option])
|
||||
else:
|
||||
await method(self._id, option)
|
||||
|
||||
|
||||
class RpcCuryModeSelect(RpcSelect):
|
||||
"""Represent a RPC select entity for Cury modes."""
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
if self.attribute_value is None:
|
||||
return "none"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self.attribute_value, str)
|
||||
|
||||
return self.attribute_value
|
||||
|
||||
|
||||
RPC_SELECT_ENTITIES: Final = {
|
||||
"cury_mode": RpcSelectDescription(
|
||||
key="cury",
|
||||
sub_key="mode",
|
||||
translation_key="cury_mode",
|
||||
options=[
|
||||
"hall",
|
||||
"bedroom",
|
||||
"living_room",
|
||||
"lavatory_room",
|
||||
"none",
|
||||
"reception",
|
||||
"workplace",
|
||||
],
|
||||
method="cury_set_mode",
|
||||
entity_class=RpcCuryModeSelect,
|
||||
),
|
||||
"enum_generic": RpcSelectDescription(
|
||||
key="enum",
|
||||
sub_key="value",
|
||||
removal_condition=lambda config, _status, key: not is_view_for_platform(
|
||||
config, key, SELECT_PLATFORM
|
||||
),
|
||||
method="enum_set",
|
||||
role=ROLE_GENERIC,
|
||||
),
|
||||
}
|
||||
@@ -167,3 +89,37 @@ def _async_setup_rpc_entry(
|
||||
virtual_text_ids,
|
||||
"enum",
|
||||
)
|
||||
|
||||
|
||||
class RpcSelect(ShellyRpcAttributeEntity, SelectEntity):
|
||||
"""Represent a RPC select entity."""
|
||||
|
||||
entity_description: RpcSelectDescription
|
||||
_id: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcSelectDescription,
|
||||
) -> None:
|
||||
"""Initialize select."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
|
||||
self._attr_options = list(self.option_map.values())
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
if not isinstance(self.attribute_value, str):
|
||||
return None
|
||||
|
||||
return self.option_map[self.attribute_value]
|
||||
|
||||
@rpc_call
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the value."""
|
||||
await self.coordinator.device.enum_set(
|
||||
self._id, self.reversed_option_map[option]
|
||||
)
|
||||
|
||||
@@ -30,12 +30,12 @@ from homeassistant.const import (
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
UnitOfVolumetricFlux,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -1465,9 +1465,9 @@ RPC_SENSORS: Final = {
|
||||
"number_last_precipitation": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
translation_key="rainfall",
|
||||
native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,
|
||||
device_class=SensorDeviceClass.PRECIPITATION_INTENSITY,
|
||||
translation_key="rainfall_last_24h",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
role="last_precipitation",
|
||||
removal_condition=lambda config, _s, _k: not config.get("service:0", {}).get(
|
||||
"weather_api", False
|
||||
@@ -1636,78 +1636,6 @@ RPC_SENSORS: Final = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="phase_info",
|
||||
),
|
||||
"object_phase_a_current": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
translation_key="current_with_phase_name",
|
||||
translation_placeholders={"phase_name": "A"},
|
||||
value=lambda status, _: float(status["phase_a"]["current"]),
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="phase_info",
|
||||
),
|
||||
"object_phase_b_current": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
translation_key="current_with_phase_name",
|
||||
translation_placeholders={"phase_name": "B"},
|
||||
value=lambda status, _: float(status["phase_b"]["current"]),
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="phase_info",
|
||||
),
|
||||
"object_phase_c_current": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
translation_key="current_with_phase_name",
|
||||
translation_placeholders={"phase_name": "C"},
|
||||
value=lambda status, _: float(status["phase_c"]["current"]),
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="phase_info",
|
||||
),
|
||||
"object_phase_a_power": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
translation_key="power_with_phase_name",
|
||||
translation_placeholders={"phase_name": "A"},
|
||||
value=lambda status, _: float(status["phase_a"]["power"]),
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="phase_info",
|
||||
),
|
||||
"object_phase_b_power": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
translation_key="power_with_phase_name",
|
||||
translation_placeholders={"phase_name": "B"},
|
||||
value=lambda status, _: float(status["phase_b"]["power"]),
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="phase_info",
|
||||
),
|
||||
"object_phase_c_power": RpcSensorDescription(
|
||||
key="object",
|
||||
sub_key="value",
|
||||
translation_key="power_with_phase_name",
|
||||
translation_placeholders={"phase_name": "C"},
|
||||
value=lambda status, _: float(status["phase_c"]["power"]),
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
role="phase_info",
|
||||
),
|
||||
"cury_left_level": RpcSensorDescription(
|
||||
key="cury",
|
||||
sub_key="slots",
|
||||
|
||||
@@ -129,32 +129,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"calibrate": {
|
||||
"name": "Calibrate"
|
||||
},
|
||||
"close": {
|
||||
"name": "Close"
|
||||
},
|
||||
"mute_alarm": {
|
||||
"name": "Mute alarm"
|
||||
},
|
||||
"open": {
|
||||
"name": "Open"
|
||||
},
|
||||
"self_test": {
|
||||
"name": "Self-test"
|
||||
},
|
||||
"turn_off_the_screen": {
|
||||
"name": "Turn off the screen"
|
||||
},
|
||||
"turn_on_the_screen": {
|
||||
"name": "Turn on the screen"
|
||||
},
|
||||
"unmute_alarm": {
|
||||
"name": "Unmute alarm"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
@@ -188,20 +162,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"cury_mode": {
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"bedroom": "Bedroom",
|
||||
"hall": "Hall",
|
||||
"lavatory_room": "Lavatory room",
|
||||
"living_room": "Living room",
|
||||
"none": "None",
|
||||
"reception": "Reception",
|
||||
"workplace": "Workplace"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"adc": {
|
||||
"name": "ADC"
|
||||
@@ -240,16 +200,13 @@
|
||||
"current_with_channel_name": {
|
||||
"name": "{channel_name} current"
|
||||
},
|
||||
"current_with_phase_name": {
|
||||
"name": "Phase {phase_name} current"
|
||||
},
|
||||
"detected_objects": {
|
||||
"name": "Detected objects",
|
||||
"unit_of_measurement": "objects"
|
||||
},
|
||||
"detected_objects_with_channel_name": {
|
||||
"name": "{channel_name} detected objects",
|
||||
"unit_of_measurement": "[%key:component::shelly::entity::sensor::detected_objects::unit_of_measurement%]"
|
||||
"unit_of_measurement": "objects"
|
||||
},
|
||||
"device_temperature": {
|
||||
"name": "Device temperature"
|
||||
@@ -334,9 +291,6 @@
|
||||
"power_with_channel_name": {
|
||||
"name": "{channel_name} power"
|
||||
},
|
||||
"power_with_phase_name": {
|
||||
"name": "Phase {phase_name} power"
|
||||
},
|
||||
"pulse_counter": {
|
||||
"name": "Pulse counter"
|
||||
},
|
||||
@@ -356,13 +310,13 @@
|
||||
"name": "Pulse counter value"
|
||||
},
|
||||
"pulse_counter_value_with_channel_name": {
|
||||
"name": "{channel_name} pulse counter value"
|
||||
"name": "{channel_name} Pulse counter value"
|
||||
},
|
||||
"pulse_counter_with_channel_name": {
|
||||
"name": "{channel_name} pulse counter"
|
||||
},
|
||||
"rainfall": {
|
||||
"name": "Rainfall"
|
||||
"rainfall_last_24h": {
|
||||
"name": "Rainfall last 24h"
|
||||
},
|
||||
"right_slot_level": {
|
||||
"name": "Right slot level"
|
||||
@@ -439,11 +393,6 @@
|
||||
"water_temperature": {
|
||||
"name": "Water temperature"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"beta_firmware": {
|
||||
"name": "Beta firmware"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -290,16 +290,6 @@ RPC_SWITCHES = {
|
||||
available=lambda status: (right := status["right"]) is not None
|
||||
and right.get("vial", {}).get("level", -1) != -1,
|
||||
),
|
||||
"cury_away_mode": RpcSwitchDescription(
|
||||
key="cury",
|
||||
sub_key="away_mode",
|
||||
name="Away mode",
|
||||
translation_key="cury_away_mode",
|
||||
is_on=lambda status: status["away_mode"],
|
||||
method_on="cury_set_away_mode",
|
||||
method_off="cury_set_away_mode",
|
||||
method_params_fn=lambda id, value: (id, value),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from .const import (
|
||||
@@ -69,6 +68,7 @@ class RestUpdateDescription(RestEntityDescription, UpdateEntityDescription):
|
||||
|
||||
REST_UPDATES: Final = {
|
||||
"fwupdate": RestUpdateDescription(
|
||||
name="Firmware",
|
||||
key="fwupdate",
|
||||
latest_version=lambda status: status["update"]["new_version"],
|
||||
beta=False,
|
||||
@@ -77,8 +77,8 @@ REST_UPDATES: Final = {
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
"fwupdate_beta": RestUpdateDescription(
|
||||
name="Beta firmware",
|
||||
key="fwupdate",
|
||||
translation_key="beta_firmware",
|
||||
latest_version=lambda status: status["update"].get("beta_version"),
|
||||
beta=True,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
@@ -89,6 +89,7 @@ REST_UPDATES: Final = {
|
||||
|
||||
RPC_UPDATES: Final = {
|
||||
"fwupdate": RpcUpdateDescription(
|
||||
name="Firmware",
|
||||
key="sys",
|
||||
sub_key="available_updates",
|
||||
latest_version=lambda status: status.get("stable", {"version": ""})["version"],
|
||||
@@ -97,9 +98,9 @@ RPC_UPDATES: Final = {
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
"fwupdate_beta": RpcUpdateDescription(
|
||||
name="Beta firmware",
|
||||
key="sys",
|
||||
sub_key="available_updates",
|
||||
translation_key="beta_firmware",
|
||||
latest_version=lambda status: status.get("beta", {"version": ""})["version"],
|
||||
beta=True,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
@@ -182,9 +183,6 @@ class RestUpdateEntity(ShellyRestAttributeEntity, UpdateEntity):
|
||||
)
|
||||
self._in_progress_old_version: str | None = None
|
||||
|
||||
if hasattr(self, "_attr_name"):
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str | None:
|
||||
"""Version currently in use."""
|
||||
@@ -278,9 +276,6 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity):
|
||||
coordinator.device.gen, coordinator.model, description.beta
|
||||
)
|
||||
|
||||
if hasattr(self, "_attr_name"):
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -376,20 +371,6 @@ class RpcSleepingUpdateEntity(
|
||||
|
||||
entity_description: RpcUpdateDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcUpdateDescription,
|
||||
entry: RegistryEntry | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sleeping sensor."""
|
||||
super().__init__(coordinator, key, attribute, description, entry)
|
||||
|
||||
if hasattr(self, "_attr_name"):
|
||||
delattr(self, "_attr_name")
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@@ -46,7 +46,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -116,13 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
||||
# after migration but still require reauthentication
|
||||
if CONF_TOKEN not in entry.data:
|
||||
raise ConfigEntryAuthFailed("Config entry missing token")
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
try:
|
||||
|
||||
@@ -661,11 +661,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation unavailable, will retry"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_binary_fridge_door": {
|
||||
"description": "The refrigerator door binary sensor {entity_name} (`{entity_id}`) is deprecated and will be removed in the future. Separate entities for cooler and freezer door are available and should be used going forward. Please update your dashboards, templates accordingly and disable the entity to fix this issue.",
|
||||
|
||||
@@ -22,7 +22,6 @@ def _get_device_class(zone_type: ZoneType) -> BinarySensorDeviceClass | None:
|
||||
return {
|
||||
ZoneType.ALARM: BinarySensorDeviceClass.MOTION,
|
||||
ZoneType.ENTRY_EXIT: BinarySensorDeviceClass.OPENING,
|
||||
ZoneType.ENTRY_EXIT_2: BinarySensorDeviceClass.OPENING,
|
||||
ZoneType.FIRE: BinarySensorDeviceClass.SMOKE,
|
||||
ZoneType.TECHNICAL: BinarySensorDeviceClass.POWER,
|
||||
}.get(zone_type)
|
||||
|
||||
@@ -13,7 +13,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -42,13 +41,7 @@ __all__ = [
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SpotifyConfigEntry) -> bool:
|
||||
"""Set up Spotify from a config entry."""
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
try:
|
||||
|
||||
@@ -32,11 +32,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation unavailable, will retry"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"api_endpoint_reachable": "Spotify API endpoint reachable"
|
||||
|
||||
@@ -616,7 +616,6 @@ class DPCode(StrEnum):
|
||||
ARM_DOWN_PERCENT = "arm_down_percent"
|
||||
ARM_UP_PERCENT = "arm_up_percent"
|
||||
ATMOSPHERIC_PRESSTURE = "atmospheric_pressture" # Typo is in Tuya API
|
||||
AUTO_CLEAN = "auto_clean"
|
||||
BACKUP_RESERVE = "backup_reserve"
|
||||
BASIC_ANTI_FLICKER = "basic_anti_flicker"
|
||||
BASIC_DEVICE_VOLUME = "basic_device_volume"
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY
|
||||
from .models import DPCodeWrapper
|
||||
|
||||
|
||||
class TuyaEntity(Entity):
|
||||
@@ -65,12 +64,3 @@ class TuyaEntity(Entity):
|
||||
"""Send command to the device."""
|
||||
LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands)
|
||||
self.device_manager.send_commands(self.device.id, commands)
|
||||
|
||||
async def _async_send_dpcode_update(
|
||||
self, dpcode_wrapper: DPCodeWrapper, value: Any
|
||||
) -> None:
|
||||
"""Send command to the device."""
|
||||
await self.hass.async_add_executor_job(
|
||||
self._send_command,
|
||||
[dpcode_wrapper.get_update_command(self.device, value)],
|
||||
)
|
||||
|
||||
@@ -14,9 +14,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import TuyaConfigEntry
|
||||
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
|
||||
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
|
||||
from .entity import TuyaEntity
|
||||
from .models import DPCodeEnumWrapper
|
||||
from .models import find_dpcode
|
||||
|
||||
# All descriptions can be found here. Mostly the Enum data types in the
|
||||
# default status set of each category (that don't have a set instruction)
|
||||
@@ -96,17 +96,10 @@ async def async_setup_entry(
|
||||
for device_id in device_ids:
|
||||
device = manager.device_map[device_id]
|
||||
if descriptions := EVENTS.get(device.category):
|
||||
entities.extend(
|
||||
TuyaEventEntity(
|
||||
device, manager, description, dpcode_wrapper=dpcode_wrapper
|
||||
)
|
||||
for description in descriptions
|
||||
if (
|
||||
dpcode_wrapper := DPCodeEnumWrapper.find_dpcode(
|
||||
device, description.key, prefer_function=True
|
||||
)
|
||||
)
|
||||
)
|
||||
for description in descriptions:
|
||||
dpcode = description.key
|
||||
if dpcode in device.status:
|
||||
entities.append(TuyaEventEntity(device, manager, description))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -127,14 +120,14 @@ class TuyaEventEntity(TuyaEntity, EventEntity):
|
||||
device: CustomerDevice,
|
||||
device_manager: Manager,
|
||||
description: EventEntityDescription,
|
||||
dpcode_wrapper: DPCodeEnumWrapper,
|
||||
) -> None:
|
||||
"""Init Tuya event entity."""
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
self._attr_event_types = dpcode_wrapper.type_information.range
|
||||
|
||||
if dpcode := find_dpcode(self.device, description.key, dptype=DPType.ENUM):
|
||||
self._attr_event_types: list[str] = dpcode.range
|
||||
|
||||
async def _handle_state_update(
|
||||
self,
|
||||
@@ -143,10 +136,10 @@ class TuyaEventEntity(TuyaEntity, EventEntity):
|
||||
) -> None:
|
||||
if (
|
||||
updated_status_properties is None
|
||||
or self._dpcode_wrapper.dpcode not in updated_status_properties
|
||||
or (value := self._dpcode_wrapper.read_device_status(self.device)) is None
|
||||
or self.entity_description.key not in updated_status_properties
|
||||
):
|
||||
return
|
||||
|
||||
value = self.device.status.get(self.entity_description.key)
|
||||
self._trigger_event(value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import base64
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
@@ -15,6 +14,137 @@ from .const import DPCode, DPType
|
||||
from .util import remap_value
|
||||
|
||||
|
||||
@dataclass
|
||||
class DPCodeWrapper:
|
||||
"""Base DPCode wrapper.
|
||||
|
||||
Used as a common interface for referring to a DPCode, and
|
||||
access read conversion routines.
|
||||
"""
|
||||
|
||||
dpcode: str
|
||||
|
||||
def _read_device_status_raw(self, device: CustomerDevice) -> Any | None:
|
||||
"""Read the raw device status for the DPCode.
|
||||
|
||||
Private helper method for `read_device_status`.
|
||||
"""
|
||||
return device.status.get(self.dpcode)
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> Any | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
raise NotImplementedError("read_device_value must be implemented")
|
||||
|
||||
|
||||
@dataclass
|
||||
class DPCodeBooleanWrapper(DPCodeWrapper):
|
||||
"""Simple wrapper for boolean values.
|
||||
|
||||
Supports True/False only.
|
||||
"""
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> bool | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := self._read_device_status_raw(device)) in (True, False):
|
||||
return raw_value
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class DPCodeEnumWrapper(DPCodeWrapper):
|
||||
"""Simple wrapper for EnumTypeData values."""
|
||||
|
||||
enum_type_information: EnumTypeData
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> str | None:
|
||||
"""Read the device value for the dpcode.
|
||||
|
||||
Values outside of the list defined by the Enum type information will
|
||||
return None.
|
||||
"""
|
||||
if (
|
||||
raw_value := self._read_device_status_raw(device)
|
||||
) in self.enum_type_information.range:
|
||||
return raw_value
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def find_dpcode(
|
||||
cls,
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...],
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
) -> Self | None:
|
||||
"""Find and return a DPCodeEnumWrapper for the given DP codes."""
|
||||
if enum_type := find_dpcode(
|
||||
device, dpcodes, dptype=DPType.ENUM, prefer_function=prefer_function
|
||||
):
|
||||
return cls(dpcode=enum_type.dpcode, enum_type_information=enum_type)
|
||||
return None
|
||||
|
||||
|
||||
@overload
|
||||
def find_dpcode(
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
dptype: Literal[DPType.ENUM],
|
||||
) -> EnumTypeData | None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def find_dpcode(
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
dptype: Literal[DPType.INTEGER],
|
||||
) -> IntegerTypeData | None: ...
|
||||
|
||||
|
||||
def find_dpcode(
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
dptype: DPType,
|
||||
) -> TypeInformation | None:
|
||||
"""Find type information for a matching DP code available for this device."""
|
||||
if not (type_information_cls := _TYPE_INFORMATION_MAPPINGS.get(dptype)):
|
||||
raise NotImplementedError(f"find_dpcode not supported for {dptype}")
|
||||
|
||||
if dpcodes is None:
|
||||
return None
|
||||
|
||||
if isinstance(dpcodes, str):
|
||||
dpcodes = (DPCode(dpcodes),)
|
||||
elif not isinstance(dpcodes, tuple):
|
||||
dpcodes = (dpcodes,)
|
||||
|
||||
lookup_tuple = (
|
||||
(device.function, device.status_range)
|
||||
if prefer_function
|
||||
else (device.status_range, device.function)
|
||||
)
|
||||
|
||||
for dpcode in dpcodes:
|
||||
for device_specs in lookup_tuple:
|
||||
if (
|
||||
(current_definition := device_specs.get(dpcode))
|
||||
and current_definition.type == dptype
|
||||
and (
|
||||
type_information := type_information_cls.from_json(
|
||||
dpcode, current_definition.values
|
||||
)
|
||||
)
|
||||
):
|
||||
return type_information
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TypeInformation:
|
||||
"""Type information.
|
||||
@@ -84,7 +214,7 @@ class IntegerTypeData(TypeInformation):
|
||||
return remap_value(value, from_min, from_max, self.min, self.max, reverse)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
|
||||
def from_json(cls, dpcode: DPCode, data: str) -> IntegerTypeData | None:
|
||||
"""Load JSON string and return a IntegerTypeData object."""
|
||||
if not (parsed := json.loads(data)):
|
||||
return None
|
||||
@@ -108,7 +238,7 @@ class EnumTypeData(TypeInformation):
|
||||
range: list[str]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
|
||||
def from_json(cls, dpcode: DPCode, data: str) -> EnumTypeData | None:
|
||||
"""Load JSON string and return a EnumTypeData object."""
|
||||
if not (parsed := json.loads(data)):
|
||||
return None
|
||||
@@ -121,218 +251,6 @@ _TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
|
||||
}
|
||||
|
||||
|
||||
class DPCodeWrapper(ABC):
|
||||
"""Base DPCode wrapper.
|
||||
|
||||
Used as a common interface for referring to a DPCode, and
|
||||
access read conversion routines.
|
||||
"""
|
||||
|
||||
def __init__(self, dpcode: str) -> None:
|
||||
"""Init DPCodeWrapper."""
|
||||
self.dpcode = dpcode
|
||||
|
||||
def _read_device_status_raw(self, device: CustomerDevice) -> Any | None:
|
||||
"""Read the raw device status for the DPCode.
|
||||
|
||||
Private helper method for `read_device_status`.
|
||||
"""
|
||||
return device.status.get(self.dpcode)
|
||||
|
||||
@abstractmethod
|
||||
def read_device_status(self, device: CustomerDevice) -> Any | None:
|
||||
"""Read the device value for the dpcode.
|
||||
|
||||
The raw device status is converted to a Home Assistant value.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
|
||||
"""Convert a Home Assistant value back to a raw device value.
|
||||
|
||||
This is called by `get_update_command` to prepare the value for sending
|
||||
back to the device, and should be implemented in concrete classes.
|
||||
"""
|
||||
|
||||
def get_update_command(self, device: CustomerDevice, value: Any) -> dict[str, Any]:
|
||||
"""Get the update command for the dpcode.
|
||||
|
||||
The Home Assistant value is converted back to a raw device value.
|
||||
"""
|
||||
return {
|
||||
"code": self.dpcode,
|
||||
"value": self._convert_value_to_raw_value(device, value),
|
||||
}
|
||||
|
||||
|
||||
class DPCodeBooleanWrapper(DPCodeWrapper):
|
||||
"""Simple wrapper for boolean values.
|
||||
|
||||
Supports True/False only.
|
||||
"""
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> bool | None:
|
||||
"""Read the device value for the dpcode."""
|
||||
if (raw_value := self._read_device_status_raw(device)) in (True, False):
|
||||
return raw_value
|
||||
return None
|
||||
|
||||
def _convert_value_to_raw_value(
|
||||
self, device: CustomerDevice, value: Any
|
||||
) -> Any | None:
|
||||
"""Convert a Home Assistant value back to a raw device value."""
|
||||
if value in (True, False):
|
||||
return value
|
||||
# Currently only called with boolean values
|
||||
# Safety net in case of future changes
|
||||
raise ValueError(f"Invalid boolean value `{value}`")
|
||||
|
||||
|
||||
class DPCodeTypeInformationWrapper[T: TypeInformation](DPCodeWrapper):
|
||||
"""Base DPCode wrapper with Type Information."""
|
||||
|
||||
DPTYPE: DPType
|
||||
type_information: T
|
||||
|
||||
def __init__(self, dpcode: str, type_information: T) -> None:
|
||||
"""Init DPCodeWrapper."""
|
||||
super().__init__(dpcode)
|
||||
self.type_information = type_information
|
||||
|
||||
@classmethod
|
||||
def find_dpcode(
|
||||
cls,
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...],
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
) -> Self | None:
|
||||
"""Find and return a DPCodeTypeInformationWrapper for the given DP codes."""
|
||||
if type_information := find_dpcode( # type: ignore[call-overload]
|
||||
device, dpcodes, dptype=cls.DPTYPE, prefer_function=prefer_function
|
||||
):
|
||||
return cls(
|
||||
dpcode=type_information.dpcode, type_information=type_information
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
|
||||
"""Simple wrapper for EnumTypeData values."""
|
||||
|
||||
DPTYPE = DPType.ENUM
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> str | None:
|
||||
"""Read the device value for the dpcode.
|
||||
|
||||
Values outside of the list defined by the Enum type information will
|
||||
return None.
|
||||
"""
|
||||
if (
|
||||
raw_value := self._read_device_status_raw(device)
|
||||
) in self.type_information.range:
|
||||
return raw_value
|
||||
return None
|
||||
|
||||
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
|
||||
"""Convert a Home Assistant value back to a raw device value."""
|
||||
if value in self.type_information.range:
|
||||
return value
|
||||
# Guarded by select option validation
|
||||
# Safety net in case of future changes
|
||||
raise ValueError(
|
||||
f"Enum value `{value}` out of range: {self.type_information.range}"
|
||||
)
|
||||
|
||||
|
||||
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
|
||||
"""Simple wrapper for IntegerTypeData values."""
|
||||
|
||||
DPTYPE = DPType.INTEGER
|
||||
|
||||
def read_device_status(self, device: CustomerDevice) -> float | None:
|
||||
"""Read the device value for the dpcode.
|
||||
|
||||
Value will be scaled based on the Integer type information.
|
||||
"""
|
||||
if (raw_value := self._read_device_status_raw(device)) is None:
|
||||
return None
|
||||
return raw_value / (10**self.type_information.scale)
|
||||
|
||||
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
|
||||
"""Convert a Home Assistant value back to a raw device value."""
|
||||
new_value = round(value * (10**self.type_information.scale))
|
||||
if self.type_information.min <= new_value <= self.type_information.max:
|
||||
return new_value
|
||||
# Guarded by number validation
|
||||
# Safety net in case of future changes
|
||||
raise ValueError(
|
||||
f"Value `{new_value}` (converted from `{value}`) out of range:"
|
||||
f" ({self.type_information.min}-{self.type_information.max})"
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def find_dpcode(
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
dptype: Literal[DPType.ENUM],
|
||||
) -> EnumTypeData | None: ...
|
||||
|
||||
|
||||
@overload
|
||||
def find_dpcode(
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
dptype: Literal[DPType.INTEGER],
|
||||
) -> IntegerTypeData | None: ...
|
||||
|
||||
|
||||
def find_dpcode(
|
||||
device: CustomerDevice,
|
||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||
*,
|
||||
prefer_function: bool = False,
|
||||
dptype: DPType,
|
||||
) -> TypeInformation | None:
|
||||
"""Find type information for a matching DP code available for this device."""
|
||||
if not (type_information_cls := _TYPE_INFORMATION_MAPPINGS.get(dptype)):
|
||||
raise NotImplementedError(f"find_dpcode not supported for {dptype}")
|
||||
|
||||
if dpcodes is None:
|
||||
return None
|
||||
|
||||
if isinstance(dpcodes, str):
|
||||
dpcodes = (DPCode(dpcodes),)
|
||||
elif not isinstance(dpcodes, tuple):
|
||||
dpcodes = (dpcodes,)
|
||||
|
||||
lookup_tuple = (
|
||||
(device.function, device.status_range)
|
||||
if prefer_function
|
||||
else (device.status_range, device.function)
|
||||
)
|
||||
|
||||
for dpcode in dpcodes:
|
||||
for device_specs in lookup_tuple:
|
||||
if (
|
||||
(current_definition := device_specs.get(dpcode))
|
||||
and current_definition.type == dptype
|
||||
and (
|
||||
type_information := type_information_cls.from_json(
|
||||
dpcode, current_definition.values
|
||||
)
|
||||
)
|
||||
):
|
||||
return type_information
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ComplexValue:
|
||||
"""Complex value (for JSON/RAW parsing)."""
|
||||
|
||||
|
||||
@@ -23,9 +23,11 @@ from .const import (
|
||||
TUYA_DISCOVERY_NEW,
|
||||
DeviceCategory,
|
||||
DPCode,
|
||||
DPType,
|
||||
)
|
||||
from .entity import TuyaEntity
|
||||
from .models import DPCodeIntegerWrapper, IntegerTypeData
|
||||
from .models import IntegerTypeData, find_dpcode
|
||||
from .util import ActionDPCodeNotFoundError
|
||||
|
||||
NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
|
||||
DeviceCategory.BH: (
|
||||
@@ -454,13 +456,9 @@ async def async_setup_entry(
|
||||
device = manager.device_map[device_id]
|
||||
if descriptions := NUMBERS.get(device.category):
|
||||
entities.extend(
|
||||
TuyaNumberEntity(device, manager, description, dpcode_wrapper)
|
||||
TuyaNumberEntity(device, manager, description)
|
||||
for description in descriptions
|
||||
if (
|
||||
dpcode_wrapper := DPCodeIntegerWrapper.find_dpcode(
|
||||
device, description.key, prefer_function=True
|
||||
)
|
||||
)
|
||||
if description.key in device.status
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
@@ -482,19 +480,21 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
|
||||
device: CustomerDevice,
|
||||
device_manager: Manager,
|
||||
description: NumberEntityDescription,
|
||||
dpcode_wrapper: DPCodeIntegerWrapper,
|
||||
) -> None:
|
||||
"""Init Tuya sensor."""
|
||||
super().__init__(device, device_manager)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
|
||||
self._attr_native_max_value = dpcode_wrapper.type_information.max_scaled
|
||||
self._attr_native_min_value = dpcode_wrapper.type_information.min_scaled
|
||||
self._attr_native_step = dpcode_wrapper.type_information.step_scaled
|
||||
if description.native_unit_of_measurement is None:
|
||||
self._attr_native_unit_of_measurement = dpcode_wrapper.type_information.unit
|
||||
if int_type := find_dpcode(
|
||||
self.device, description.key, dptype=DPType.INTEGER, prefer_function=True
|
||||
):
|
||||
self._number = int_type
|
||||
self._attr_native_max_value = self._number.max_scaled
|
||||
self._attr_native_min_value = self._number.min_scaled
|
||||
self._attr_native_step = self._number.step_scaled
|
||||
if description.native_unit_of_measurement is None:
|
||||
self._attr_native_unit_of_measurement = int_type.unit
|
||||
|
||||
# Logic to ensure the set device class and API received Unit Of Measurement
|
||||
# match Home Assistants requirements.
|
||||
@@ -538,8 +538,26 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self._dpcode_wrapper.read_device_status(self.device)
|
||||
# Unknown or unsupported data type
|
||||
if self._number is None:
|
||||
return None
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
# Raw value
|
||||
if (value := self.device.status.get(self.entity_description.key)) is None:
|
||||
return None
|
||||
|
||||
return self._number.scale_value(value)
|
||||
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
await self._async_send_dpcode_update(self._dpcode_wrapper, value)
|
||||
if self._number is None:
|
||||
raise ActionDPCodeNotFoundError(self.device, self.entity_description.key)
|
||||
|
||||
self._send_command(
|
||||
[
|
||||
{
|
||||
"code": self.entity_description.key,
|
||||
"value": self._number.scale_value_back(value),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
@@ -395,13 +395,13 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||
self._dpcode_wrapper = dpcode_wrapper
|
||||
self._attr_options = dpcode_wrapper.type_information.range
|
||||
self._attr_options = dpcode_wrapper.enum_type_information.range
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the selected entity option to represent the entity state."""
|
||||
return self._dpcode_wrapper.read_device_status(self.device)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self._async_send_dpcode_update(self._dpcode_wrapper, option)
|
||||
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": option}])
|
||||
|
||||
@@ -886,9 +886,6 @@
|
||||
"arm_beep": {
|
||||
"name": "Arm beep"
|
||||
},
|
||||
"auto_clean": {
|
||||
"name": "Auto clean"
|
||||
},
|
||||
"battery_lock": {
|
||||
"name": "Battery lock"
|
||||
},
|
||||
|
||||
@@ -523,13 +523,6 @@ SWITCHES: dict[DeviceCategory, tuple[SwitchEntityDescription, ...]] = {
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
DeviceCategory.MSP: (
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.AUTO_CLEAN,
|
||||
translation_key="auto_clean",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
DeviceCategory.MZJ: (
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.SWITCH,
|
||||
@@ -1041,10 +1034,10 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
|
||||
"""Return true if switch is on."""
|
||||
return self._dpcode_wrapper.read_device_status(self.device)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self._async_send_dpcode_update(self._dpcode_wrapper, True)
|
||||
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": True}])
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self._async_send_dpcode_update(self._dpcode_wrapper, False)
|
||||
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": False}])
|
||||
|
||||
@@ -140,8 +140,12 @@ class TuyaValveEntity(TuyaEntity, ValveEntity):
|
||||
|
||||
async def async_open_valve(self) -> None:
|
||||
"""Open the valve."""
|
||||
await self._async_send_dpcode_update(self._dpcode_wrapper, True)
|
||||
await self.hass.async_add_executor_job(
|
||||
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": True}]
|
||||
)
|
||||
|
||||
async def async_close_valve(self) -> None:
|
||||
"""Close the valve."""
|
||||
await self._async_send_dpcode_update(self._dpcode_wrapper, False)
|
||||
await self.hass.async_add_executor_job(
|
||||
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": False}]
|
||||
)
|
||||
|
||||
@@ -11,28 +11,21 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
LocalOAuth2Implementation,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, OAUTH_SCOPES, PLATFORMS
|
||||
from .const import OAUTH_SCOPES, PLATFORMS
|
||||
from .coordinator import TwitchConfigEntry, TwitchCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: TwitchConfigEntry) -> bool:
|
||||
"""Set up Twitch from a config entry."""
|
||||
try:
|
||||
implementation = cast(
|
||||
LocalOAuth2Implementation,
|
||||
await async_get_config_entry_implementation(hass, entry),
|
||||
)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = cast(
|
||||
LocalOAuth2Implementation,
|
||||
await async_get_config_entry_implementation(hass, entry),
|
||||
)
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
try:
|
||||
await session.async_ensure_token_valid()
|
||||
|
||||
@@ -58,10 +58,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation unavailable, will retry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class VeSyncHumidifierHA(VeSyncBaseEntity, HumidifierEntity):
|
||||
raise HomeAssistantError(
|
||||
f"Invalid mode {mode}. Available modes: {self.available_modes}"
|
||||
)
|
||||
if not await self.device.set_mode(self._get_vs_mode(mode)):
|
||||
if not await self.device.set_humidity_mode(self._get_vs_mode(mode)):
|
||||
raise HomeAssistantError(self.device.last_response.message)
|
||||
|
||||
if mode == MODE_SLEEP:
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/vesync",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyvesync"],
|
||||
"requirements": ["pyvesync==3.2.1"]
|
||||
"requirements": ["pyvesync==3.1.4"]
|
||||
}
|
||||
|
||||
@@ -37,10 +37,8 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
@@ -106,13 +104,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: WithingsConfigEntry) ->
|
||||
)
|
||||
session = async_get_clientsession(hass)
|
||||
client = WithingsClient(session=session)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
refresh_lock = asyncio.Lock()
|
||||
|
||||
@@ -331,10 +331,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation unavailable, will retry"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""API for xbox bound to Home Assistant OAuth."""
|
||||
|
||||
from httpx import AsyncClient
|
||||
from pythonxbox.authentication.manager import AuthenticationManager
|
||||
from pythonxbox.authentication.models import OAuth2TokenResponse
|
||||
from pythonxbox.common.signed_session import SignedSession
|
||||
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
@@ -11,10 +11,12 @@ from homeassistant.util.dt import utc_from_timestamp
|
||||
class AsyncConfigEntryAuth(AuthenticationManager):
|
||||
"""Provide xbox authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(self, session: AsyncClient, oauth_session: OAuth2Session) -> None:
|
||||
def __init__(
|
||||
self, signed_session: SignedSession, oauth_session: OAuth2Session
|
||||
) -> None:
|
||||
"""Initialize xbox auth."""
|
||||
# Leaving out client credentials as they are handled by Home Assistant
|
||||
super().__init__(session, "", "", "")
|
||||
super().__init__(signed_session, "", "", "")
|
||||
self._oauth_session = oauth_session
|
||||
self.oauth = self._get_oauth_token()
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from httpx import AsyncClient
|
||||
from pythonxbox.api.client import XboxLiveClient
|
||||
from pythonxbox.authentication.manager import AuthenticationManager
|
||||
from pythonxbox.authentication.models import OAuth2TokenResponse
|
||||
from pythonxbox.common.signed_session import SignedSession
|
||||
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
@@ -43,7 +43,7 @@ class OAuth2FlowHandler(
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow."""
|
||||
|
||||
async with AsyncClient() as session:
|
||||
async with SignedSession() as session:
|
||||
auth = AuthenticationManager(session, "", "", "")
|
||||
auth.oauth = OAuth2TokenResponse(**data["token"])
|
||||
await auth.refresh_tokens()
|
||||
|
||||
@@ -17,18 +17,14 @@ from pythonxbox.api.provider.smartglass.models import (
|
||||
SmartglassConsoleStatus,
|
||||
)
|
||||
from pythonxbox.api.provider.titlehub.models import Title
|
||||
from pythonxbox.common.signed_session import SignedSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
@@ -81,18 +77,22 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up coordinator."""
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(
|
||||
self.hass, self.config_entry
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
self.hass, self.config_entry
|
||||
)
|
||||
)
|
||||
except ImplementationUnavailableError as e:
|
||||
except ValueError as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
translation_key="request_exception",
|
||||
) from e
|
||||
|
||||
session = OAuth2Session(self.hass, self.config_entry, implementation)
|
||||
async_session = get_async_client(self.hass)
|
||||
auth = api.AsyncConfigEntryAuth(async_session, session)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(
|
||||
self.hass, self.config_entry, implementation
|
||||
)
|
||||
signed_session = SignedSession(ssl_context=get_default_context())
|
||||
auth = api.AsyncConfigEntryAuth(signed_session, session)
|
||||
self.client = XboxLiveClient(auth)
|
||||
|
||||
try:
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/xbox",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-xbox==0.1.1"],
|
||||
"requirements": ["python-xbox==0.1.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Microsoft Corporation",
|
||||
|
||||
@@ -98,9 +98,6 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation temporarily unavailable, will retry"
|
||||
},
|
||||
"request_exception": {
|
||||
"message": "Failed to connect to Xbox Network"
|
||||
},
|
||||
|
||||
@@ -16,12 +16,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .data import YaleData
|
||||
@@ -35,10 +30,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
|
||||
"""Set up Yale from a config entry."""
|
||||
session = async_create_yale_clientsession(hass)
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ConfigEntryNotReady("OAuth implementation not available") from err
|
||||
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||
oauth_session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_yale(hass, entry, yale_gateway)
|
||||
|
||||
@@ -19,14 +19,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_entry_oauth2_flow,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
)
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import api
|
||||
@@ -124,15 +120,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up yolink from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
try:
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
|
||||
auth_mgr = api.ConfigEntryAuth(
|
||||
hass, aiohttp_client.async_get_clientsession(hass), session
|
||||
|
||||
@@ -133,9 +133,6 @@
|
||||
"invalid_config_entry": {
|
||||
"message": "Config entry not found or not loaded!"
|
||||
},
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "OAuth2 implementation temporarily unavailable, will retry"
|
||||
},
|
||||
"valve_inoperable_currently": {
|
||||
"message": "The Valve cannot be operated currently."
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user