Compare commits

..

18 Commits

Author SHA1 Message Date
jbouwh
49db8800cc Improve docstring 2025-11-10 11:16:51 +00:00
jbouwh
a3752e1ee3 Remove _included_entities property 2025-11-10 11:14:10 +00:00
jbouwh
a1eada037a Do not set included entities if no unique IDs are set 2025-11-08 08:55:46 +00:00
jbouwh
ba076de313 Upfdate docstr 2025-11-08 08:55:46 +00:00
jbouwh
5f18d60afb Call async_set_included_entities from add_to_platform_finish 2025-11-08 08:55:46 +00:00
jbouwh
341d38e961 Handle the entity_id attribute in the Entity base class 2025-11-08 08:55:46 +00:00
jbouwh
706abf52c2 Fix device tracker 2025-11-08 08:55:46 +00:00
jbouwh
74c86ac0c1 Use platform name 2025-11-08 08:55:46 +00:00
jbouwh
388a396519 Fix device tracker state attrs 2025-11-08 08:55:45 +00:00
jbouwh
f9d2d69d78 Also implement as default in base entity 2025-11-08 08:55:45 +00:00
jbouwh
eab86c4320 Integrate with base entity component state attributes 2025-11-08 08:55:45 +00:00
jbouwh
e365c86c92 Update docstr 2025-11-08 08:55:45 +00:00
jbouwh
8e7ffd7d90 Move logic into Entity class 2025-11-08 08:55:45 +00:00
jbouwh
1318cad084 Use platform domain attribute 2025-11-08 08:55:45 +00:00
jbouwh
7e81e406ac Fix typo 2025-11-08 08:55:45 +00:00
jbouwh
1fe3892c83 Follow up on code review 2025-11-08 08:55:45 +00:00
jbouwh
89aed81ae0 Implement mixin class and add feature to maintain included entities from unique IDs 2025-11-08 08:55:45 +00:00
jbouwh
f3a2d060a5 Add included_entities attribute to base Entity class 2025-11-08 08:55:45 +00:00
195 changed files with 1522 additions and 4863 deletions

View File

@@ -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

View File

@@ -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(

View File

@@ -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"])

View File

@@ -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

View File

@@ -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"
}
}
}
}

View File

@@ -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)

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["avea"],
"quality_scale": "legacy",
"requirements": ["avea==1.6.1"]
"requirements": ["avea==1.5.1"]
}

View File

@@ -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"
]
}

View File

@@ -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"]
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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]

View File

@@ -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."""

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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)

View File

@@ -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]})

View File

@@ -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"]
}

View File

@@ -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"

View File

@@ -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(

View File

@@ -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.

View File

@@ -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(

View File

@@ -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"
],

View File

@@ -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")

View File

@@ -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"

View File

@@ -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()

View File

@@ -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

View File

@@ -9,5 +9,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["apyhiveapi"],
"requirements": ["pyhive-integration==1.0.6"]
"requirements": ["pyhive-integration==1.0.2"]
}

View File

@@ -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:

View File

@@ -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}"
},

View File

@@ -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"]
}

View File

@@ -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,
)

View File

@@ -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"
},

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)"
},

View File

@@ -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"
],

View File

@@ -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)

View File

@@ -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}"
},

View File

@@ -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,

View File

@@ -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"
}
}
},

View File

@@ -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()

View File

@@ -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."
},

View File

@@ -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:

View File

@@ -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"
},

View File

@@ -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."]
}

View File

@@ -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",

View File

@@ -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,
)
}

View File

@@ -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",

View File

@@ -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)
)
]
)

View File

@@ -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",

View File

@@ -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] = {

View File

@@ -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"

View File

@@ -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

View File

@@ -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,

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -33,10 +33,5 @@
"title": "[%key:common::config_flow::title::reauth%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "OAuth2 implementation unavailable, will retry"
}
}
}

View File

@@ -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,
),

View File

@@ -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"
]
}

View File

@@ -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

View File

@@ -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",
),
}

View File

@@ -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"

View File

@@ -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"
},

View File

@@ -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*",

View File

@@ -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]
)

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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),
),
}

View File

@@ -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()

View File

@@ -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:

View File

@@ -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.",

View File

@@ -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)

View File

@@ -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:

View File

@@ -32,11 +32,6 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "OAuth2 implementation unavailable, will retry"
}
},
"system_health": {
"info": {
"api_endpoint_reachable": "Spotify API endpoint reachable"

View File

@@ -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"

View File

@@ -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)],
)

View File

@@ -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()

View File

@@ -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)."""

View File

@@ -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),
}
]
)

View File

@@ -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}])

View File

@@ -886,9 +886,6 @@
"arm_beep": {
"name": "Arm beep"
},
"auto_clean": {
"name": "Auto clean"
},
"battery_lock": {
"name": "Battery lock"
},

View File

@@ -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}])

View File

@@ -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}]
)

View File

@@ -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()

View File

@@ -58,10 +58,5 @@
}
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "OAuth2 implementation unavailable, will retry"
}
}
}

View File

@@ -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:

View File

@@ -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"]
}

View File

@@ -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()

View File

@@ -331,10 +331,5 @@
}
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "OAuth2 implementation unavailable, will retry"
}
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:

View File

@@ -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",

View File

@@ -98,9 +98,6 @@
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "OAuth2 implementation temporarily unavailable, will retry"
},
"request_exception": {
"message": "Failed to connect to Xbox Network"
},

View File

@@ -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)

View File

@@ -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

View File

@@ -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