mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Merge branch 'dev' into prepare_protobuf6
This commit is contained in:
commit
3847f084ac
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -653,7 +653,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4.5.0
|
||||
uses: actions/dependency-review-action@v4.6.0
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
|
@ -364,6 +364,7 @@ homeassistant.components.notify.*
|
||||
homeassistant.components.notion.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.nut.*
|
||||
homeassistant.components.ohme.*
|
||||
homeassistant.components.onboarding.*
|
||||
homeassistant.components.oncue.*
|
||||
homeassistant.components.onedrive.*
|
||||
|
@ -859,8 +859,14 @@ async def _async_set_up_integrations(
|
||||
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
||||
hass, config
|
||||
)
|
||||
all_domains = set(all_integrations)
|
||||
domains = set(integrations)
|
||||
# Detect all cycles
|
||||
integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, all_integrations.values(), set(all_integrations)
|
||||
)
|
||||
)
|
||||
all_domains = set(integrations_after_dependencies)
|
||||
domains = set(integrations) & all_domains
|
||||
|
||||
_LOGGER.info(
|
||||
"Domains to be set up: %s | %s",
|
||||
@ -868,6 +874,8 @@ async def _async_set_up_integrations(
|
||||
all_domains - domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, all_domains)
|
||||
|
||||
# Initialize recorder
|
||||
if "recorder" in all_domains:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
@ -900,24 +908,12 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered = {
|
||||
dep
|
||||
for domain in stage_domains
|
||||
for dep in all_integrations[domain].all_dependencies
|
||||
for dep in integrations_after_dependencies[domain]
|
||||
if dep not in stage_domains
|
||||
}
|
||||
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
|
||||
|
||||
stage_all_domains = stage_domains | stage_dep_domains
|
||||
stage_all_integrations = {
|
||||
domain: all_integrations[domain] for domain in stage_all_domains
|
||||
}
|
||||
# Detect all cycles
|
||||
stage_integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, stage_all_integrations.values(), stage_all_domains
|
||||
)
|
||||
)
|
||||
stage_all_domains = set(stage_integrations_after_dependencies)
|
||||
stage_domains &= stage_all_domains
|
||||
stage_dep_domains &= stage_all_domains
|
||||
|
||||
_LOGGER.info(
|
||||
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
||||
@ -928,8 +924,6 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered - stage_dep_domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, stage_all_domains)
|
||||
|
||||
if timeout is None:
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
|
5
homeassistant/brands/eve.json
Normal file
5
homeassistant/brands/eve.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "eve",
|
||||
"name": "Eve",
|
||||
"iot_standards": ["matter"]
|
||||
}
|
@ -72,10 +72,10 @@
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"state": {
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "Moderate",
|
||||
"very_high": "Very high"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,10 +89,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -123,10 +123,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -167,10 +167,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,10 +181,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -195,10 +195,10 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::high%]",
|
||||
"low": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::low%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
"very_high": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::very_high%]"
|
||||
"very_high": "[%key:common::state::very_high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.9.9"]
|
||||
"requirements": ["aioairzone==1.0.0"]
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ from aioairzone.const import (
|
||||
AZD_HUMIDITY,
|
||||
AZD_TEMP,
|
||||
AZD_TEMP_UNIT,
|
||||
AZD_THERMOSTAT_BATTERY,
|
||||
AZD_THERMOSTAT_SIGNAL,
|
||||
AZD_WEBSERVER,
|
||||
AZD_WIFI_RSSI,
|
||||
AZD_ZONES,
|
||||
@ -73,6 +75,20 @@ ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
key=AZD_THERMOSTAT_BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
key=AZD_THERMOSTAT_SIGNAL,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="thermostat_signal",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -76,6 +76,9 @@
|
||||
"sensor": {
|
||||
"rssi": {
|
||||
"name": "RSSI"
|
||||
},
|
||||
"thermostat_signal": {
|
||||
"name": "Signal strength"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_IDENTIFIERS: list(combined_identifiers),
|
||||
},
|
||||
)
|
||||
if entry.source != SOURCE_IGNORE:
|
||||
# Don't reload ignored entries or in the middle of reauth,
|
||||
# e.g. if the user is entering a new PIN
|
||||
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
if not allow_exist:
|
||||
raise DeviceAlreadyConfigured
|
||||
|
@ -36,9 +36,9 @@
|
||||
"wi_fi_strength": {
|
||||
"name": "Wi-Fi strength",
|
||||
"state": {
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High"
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("message", "media_id"),
|
||||
@ -75,7 +76,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("start_message"): str,
|
||||
vol.Optional("start_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
),
|
||||
|
@ -180,7 +180,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self,
|
||||
message: str | None = None,
|
||||
media_id: str | None = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Play and show an announcement on the satellite.
|
||||
|
||||
@ -190,8 +191,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
If media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce is True, a sound is played before the announcement.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
If preannounce_media_id is None, no sound is played.
|
||||
|
||||
Calls async_announce with message and media id.
|
||||
"""
|
||||
@ -201,7 +202,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
message = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
message, media_id, preannounce_media_id
|
||||
message,
|
||||
media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
@ -229,7 +232,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message: str | None = None,
|
||||
start_media_id: str | None = None,
|
||||
extra_system_prompt: str | None = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Start a conversation from the satellite.
|
||||
|
||||
@ -239,8 +243,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
If start_media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce_media_id is provided, it is played before the announcement.
|
||||
If preannounce_media_id is None, no sound is played.
|
||||
If preannounce is True, a sound is played before the start message or media.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
|
||||
Calls async_start_conversation.
|
||||
"""
|
||||
@ -257,7 +261,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
start_message, start_media_id, preannounce_media_id
|
||||
start_message,
|
||||
start_media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
|
@ -15,6 +15,11 @@ announce:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
@ -40,6 +45,11 @@ start_conversation:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
|
@ -24,9 +24,13 @@
|
||||
"name": "Media ID",
|
||||
"description": "The media ID to announce instead of using text-to-speech."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the announcement."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce Media ID",
|
||||
"description": "The media ID to play before the announcement."
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the announcement."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -46,9 +50,13 @@
|
||||
"name": "Extra system prompt",
|
||||
"description": "Provide background information to the AI about the request."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the start message or media."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce Media ID",
|
||||
"description": "The media ID to play before the start message or media."
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,7 +199,7 @@ async def websocket_test_connection(
|
||||
hass.async_create_background_task(
|
||||
satellite.async_internal_announce(
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
|
||||
preannounce_media_id=None,
|
||||
preannounce=False,
|
||||
),
|
||||
f"assist_satellite_connection_test_{msg['entity_id']}",
|
||||
)
|
||||
|
@ -103,8 +103,8 @@
|
||||
"temperature_range": {
|
||||
"name": "Temperature range",
|
||||
"state": {
|
||||
"low": "Low",
|
||||
"high": "High"
|
||||
"low": "[%key:common::state::low%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -124,8 +124,8 @@
|
||||
"battery": {
|
||||
"name": "Battery",
|
||||
"state": {
|
||||
"off": "Normal",
|
||||
"on": "Low"
|
||||
"off": "[%key:common::state::normal%]",
|
||||
"on": "[%key:common::state::low%]"
|
||||
}
|
||||
},
|
||||
"battery_charging": {
|
||||
@ -145,7 +145,7 @@
|
||||
"cold": {
|
||||
"name": "Cold",
|
||||
"state": {
|
||||
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]",
|
||||
"off": "[%key:common::state::normal%]",
|
||||
"on": "Cold"
|
||||
}
|
||||
},
|
||||
@ -180,7 +180,7 @@
|
||||
"heat": {
|
||||
"name": "Heat",
|
||||
"state": {
|
||||
"off": "[%key:component::binary_sensor::entity_component::battery::state::off%]",
|
||||
"off": "[%key:common::state::normal%]",
|
||||
"on": "Hot"
|
||||
}
|
||||
},
|
||||
|
@ -501,18 +501,16 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
|
||||
return
|
||||
|
||||
# presets and inputs might have the same name; presets have priority
|
||||
url: str | None = None
|
||||
for input_ in self._inputs:
|
||||
if input_.text == source:
|
||||
url = input_.url
|
||||
await self._player.play_url(input_.url)
|
||||
return
|
||||
for preset in self._presets:
|
||||
if preset.name == source:
|
||||
url = preset.url
|
||||
await self._player.load_preset(preset.id)
|
||||
return
|
||||
|
||||
if url is None:
|
||||
raise ServiceValidationError(f"Source {source} not found")
|
||||
|
||||
await self._player.play_url(url)
|
||||
raise ServiceValidationError(f"Source {source} not found")
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
"""Clear players playlist."""
|
||||
|
@ -19,7 +19,7 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-data-tools==1.26.1",
|
||||
"bluetooth-data-tools==1.27.0",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.37.0"
|
||||
]
|
||||
|
@ -16,6 +16,7 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
@ -91,11 +92,22 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._discovered[CONF_ACCESS_TOKEN] = token
|
||||
try:
|
||||
_, hub_name = await _validate_input(self.hass, self._discovered)
|
||||
bond_id, hub_name = await _validate_input(self.hass, self._discovered)
|
||||
except InputValidationError:
|
||||
return
|
||||
await self.async_set_unique_id(bond_id)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
|
||||
self._discovered[CONF_NAME] = hub_name
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by dhcp discovery."""
|
||||
host = discovery_info.ip
|
||||
bond_id = discovery_info.hostname.partition("-")[2].upper()
|
||||
await self.async_set_unique_id(bond_id)
|
||||
return await self.async_step_any_discovery(bond_id, host)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
@ -104,11 +116,17 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
host: str = discovery_info.host
|
||||
bond_id = name.partition(".")[0]
|
||||
await self.async_set_unique_id(bond_id)
|
||||
return await self.async_step_any_discovery(bond_id, host)
|
||||
|
||||
async def async_step_any_discovery(
|
||||
self, bond_id: str, host: str
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
for entry in self._async_current_entries():
|
||||
if entry.unique_id != bond_id:
|
||||
continue
|
||||
updates = {CONF_HOST: host}
|
||||
if entry.state == ConfigEntryState.SETUP_ERROR and (
|
||||
if entry.state is ConfigEntryState.SETUP_ERROR and (
|
||||
token := await async_get_token(self.hass, host)
|
||||
):
|
||||
updates[CONF_ACCESS_TOKEN] = token
|
||||
@ -153,10 +171,14 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_HOST: self._discovered[CONF_HOST],
|
||||
}
|
||||
try:
|
||||
_, hub_name = await _validate_input(self.hass, data)
|
||||
bond_id, hub_name = await _validate_input(self.hass, data)
|
||||
except InputValidationError as error:
|
||||
errors["base"] = error.base
|
||||
else:
|
||||
await self.async_set_unique_id(bond_id)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self._discovered[CONF_HOST]}
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=hub_name,
|
||||
data=data,
|
||||
@ -185,8 +207,10 @@ class BondConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
except InputValidationError as error:
|
||||
errors["base"] = error.base
|
||||
else:
|
||||
await self.async_set_unique_id(bond_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
await self.async_set_unique_id(bond_id, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: user_input[CONF_HOST]}
|
||||
)
|
||||
return self.async_create_entry(title=hub_name, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@ -3,6 +3,16 @@
|
||||
"name": "Bond",
|
||||
"codeowners": ["@bdraco", "@prystupa", "@joshs85", "@marciogranzotto"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"hostname": "bond-*",
|
||||
"macaddress": "3C6A2C1*"
|
||||
},
|
||||
{
|
||||
"hostname": "bond-*",
|
||||
"macaddress": "F44E38*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bond",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["bond_async"],
|
||||
|
@ -9,7 +9,7 @@ from bosch_alarm_mode2 import Panel
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||
@ -34,10 +34,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
|
||||
await panel.connect()
|
||||
except (PermissionError, ValueError) as err:
|
||||
await panel.disconnect()
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from err
|
||||
except (TimeoutError, OSError, ConnectionRefusedError, SSLError) as err:
|
||||
await panel.disconnect()
|
||||
raise ConfigEntryNotReady("Connection failed") from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
entry.runtime_data = panel
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
import ssl
|
||||
from typing import Any
|
||||
@ -163,3 +164,55 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=self.add_suggested_values_to_schema(schema, user_input),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an authentication error."""
|
||||
self._data = dict(entry_data)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the reauth step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
# Each model variant requires a different authentication flow
|
||||
if "Solution" in self._data[CONF_MODEL]:
|
||||
schema = STEP_AUTH_DATA_SCHEMA_SOLUTION
|
||||
elif "AMAX" in self._data[CONF_MODEL]:
|
||||
schema = STEP_AUTH_DATA_SCHEMA_AMAX
|
||||
else:
|
||||
schema = STEP_AUTH_DATA_SCHEMA_BG
|
||||
|
||||
if user_input is not None:
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
self._data.update(user_input)
|
||||
try:
|
||||
(_, _) = await try_connect(self._data, Panel.LOAD_EXTENDED_INFO)
|
||||
except (PermissionError, ValueError) as e:
|
||||
errors["base"] = "invalid_auth"
|
||||
_LOGGER.error("Authentication Error: %s", e)
|
||||
except (
|
||||
OSError,
|
||||
ConnectionRefusedError,
|
||||
ssl.SSLError,
|
||||
TimeoutError,
|
||||
) as e:
|
||||
_LOGGER.error("Connection Error: %s", e)
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(schema, user_input),
|
||||
errors=errors,
|
||||
)
|
||||
|
73
homeassistant/components/bosch_alarm/diagnostics.py
Normal file
73
homeassistant/components/bosch_alarm/diagnostics.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""Diagnostics for bosch alarm."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import BoschAlarmConfigEntry
|
||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE
|
||||
|
||||
TO_REDACT = [CONF_INSTALLER_CODE, CONF_USER_CODE, CONF_PASSWORD]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: BoschAlarmConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||
"data": {
|
||||
"model": entry.runtime_data.model,
|
||||
"serial_number": entry.runtime_data.serial_number,
|
||||
"protocol_version": entry.runtime_data.protocol_version,
|
||||
"firmware_version": entry.runtime_data.firmware_version,
|
||||
"areas": [
|
||||
{
|
||||
"id": area_id,
|
||||
"name": area.name,
|
||||
"all_ready": area.all_ready,
|
||||
"part_ready": area.part_ready,
|
||||
"faults": area.faults,
|
||||
"alarms": area.alarms,
|
||||
"disarmed": area.is_disarmed(),
|
||||
"arming": area.is_arming(),
|
||||
"pending": area.is_pending(),
|
||||
"part_armed": area.is_part_armed(),
|
||||
"all_armed": area.is_all_armed(),
|
||||
"armed": area.is_armed(),
|
||||
"triggered": area.is_triggered(),
|
||||
}
|
||||
for area_id, area in entry.runtime_data.areas.items()
|
||||
],
|
||||
"points": [
|
||||
{
|
||||
"id": point_id,
|
||||
"name": point.name,
|
||||
"open": point.is_open(),
|
||||
"normal": point.is_normal(),
|
||||
}
|
||||
for point_id, point in entry.runtime_data.points.items()
|
||||
],
|
||||
"doors": [
|
||||
{
|
||||
"id": door_id,
|
||||
"name": door.name,
|
||||
"open": door.is_open(),
|
||||
"locked": door.is_locked(),
|
||||
}
|
||||
for door_id, door in entry.runtime_data.doors.items()
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"id": output_id,
|
||||
"name": output.name,
|
||||
"active": output.is_active(),
|
||||
}
|
||||
for output_id, output in entry.runtime_data.outputs.items()
|
||||
],
|
||||
"history_events": entry.runtime_data.events,
|
||||
},
|
||||
}
|
@ -40,7 +40,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
|
@ -22,6 +22,18 @@
|
||||
"installer_code": "The installer code from your panel",
|
||||
"user_code": "The user code from your panel"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data::installer_code%]",
|
||||
"user_code": "[%key:component::bosch_alarm::config::step::auth::data::user_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::bosch_alarm::config::step::auth::data_description::password%]",
|
||||
"installer_code": "[%key:component::bosch_alarm::config::step::auth::data_description::installer_code%]",
|
||||
"user_code": "[%key:component::bosch_alarm::config::step::auth::data_description::user_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@ -30,7 +42,16 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Could not connect to panel."
|
||||
},
|
||||
"authentication_failed": {
|
||||
"message": "Incorrect credentials for panel."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@
|
||||
},
|
||||
"get_events": {
|
||||
"name": "Get events",
|
||||
"description": "Get events on a calendar within a time range.",
|
||||
"description": "Retrieves events on a calendar within a time range.",
|
||||
"fields": {
|
||||
"start_date_time": {
|
||||
"name": "Start time",
|
||||
|
@ -2,17 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
with suppress(Exception):
|
||||
# TurboJPEG imports numpy which may or may not work so
|
||||
# we have to guard the import here. We still want
|
||||
# to import it at top level so it gets loaded
|
||||
# in the import executor and not in the event loop.
|
||||
from turbojpeg import TurboJPEG
|
||||
|
||||
from turbojpeg import TurboJPEG
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Image
|
||||
|
@ -127,7 +127,11 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
|
||||
flow_id=flow_id, user_input=tokens
|
||||
)
|
||||
|
||||
self.hass.async_create_task(await_tokens())
|
||||
# It's a background task because it should be cancelled on shutdown and there's nothing else
|
||||
# we can do in such case. There's also no need to wait for this during setup.
|
||||
self.hass.async_create_background_task(
|
||||
await_tokens(), name="Awaiting OAuth tokens"
|
||||
)
|
||||
|
||||
return authorize_url
|
||||
|
||||
|
@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
if self.mode == HumidifierComelitMode.OFF:
|
||||
if not self._attr_is_on:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="humidity_while_off",
|
||||
@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, self._set_command
|
||||
)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, HumidifierComelitCommand.OFF
|
||||
)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
@ -42,9 +42,9 @@
|
||||
"sensor": {
|
||||
"zone_status": {
|
||||
"state": {
|
||||
"open": "[%key:common::state::open%]",
|
||||
"alarm": "Alarm",
|
||||
"armed": "Armed",
|
||||
"open": "Open",
|
||||
"excluded": "Excluded",
|
||||
"faulty": "Faulty",
|
||||
"inhibited": "Inhibited",
|
||||
@ -52,7 +52,9 @@
|
||||
"rest": "Rest",
|
||||
"sabotated": "Sabotated"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"humidifier": {
|
||||
"name": "Humidifier"
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.exceptions import 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.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.util.ssl import client_context_no_verify
|
||||
|
||||
from .const import KEY_MAC, TIMEOUT
|
||||
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
||||
@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
|
||||
key=entry.data.get(CONF_API_KEY),
|
||||
uuid=entry.data.get(CONF_UUID),
|
||||
password=entry.data.get(CONF_PASSWORD),
|
||||
ssl_context=client_context_no_verify(),
|
||||
)
|
||||
_LOGGER.debug("Connection to %s successful", host)
|
||||
except TimeoutError as err:
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.14.1"],
|
||||
"requirements": ["pydaikin==2.15.0"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls",
|
||||
"description": "To be able to use this integration, you have to enable the following option in Deluge settings: Daemon > Allow remote controls",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.43.0", "getmac==0.9.5"],
|
||||
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"dependencies": ["ssdp"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["async-upnp-client==0.43.0"],
|
||||
"requirements": ["async-upnp-client==0.44.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["dsmr_parser"],
|
||||
"requirements": ["dsmr-parser==1.4.2"]
|
||||
"requirements": ["dsmr-parser==1.4.3"]
|
||||
}
|
||||
|
@ -51,8 +51,8 @@
|
||||
"electricity_active_tariff": {
|
||||
"name": "Active tariff",
|
||||
"state": {
|
||||
"low": "Low",
|
||||
"normal": "Normal"
|
||||
"low": "[%key:common::state::low%]",
|
||||
"normal": "[%key:common::state::normal%]"
|
||||
}
|
||||
},
|
||||
"electricity_delivered_tariff_1": {
|
||||
|
@ -140,8 +140,8 @@
|
||||
"electricity_tariff": {
|
||||
"name": "Electricity tariff",
|
||||
"state": {
|
||||
"low": "Low",
|
||||
"high": "High"
|
||||
"low": "[%key:common::state::low%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
}
|
||||
},
|
||||
"power_failure_count": {
|
||||
|
@ -55,7 +55,7 @@
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Ecobee thermostat on which to create the vacation."
|
||||
"description": "ecobee thermostat on which to create the vacation."
|
||||
},
|
||||
"vacation_name": {
|
||||
"name": "Vacation name",
|
||||
@ -101,7 +101,7 @@
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Ecobee thermostat on which to delete the vacation."
|
||||
"description": "ecobee thermostat on which to delete the vacation."
|
||||
},
|
||||
"vacation_name": {
|
||||
"name": "[%key:component::ecobee::services::create_vacation::fields::vacation_name::name%]",
|
||||
@ -149,7 +149,7 @@
|
||||
},
|
||||
"set_mic_mode": {
|
||||
"name": "Set mic mode",
|
||||
"description": "Enables/disables Alexa microphone (only for Ecobee 4).",
|
||||
"description": "Enables/disables Alexa microphone (only for ecobee 4).",
|
||||
"fields": {
|
||||
"mic_enabled": {
|
||||
"name": "Mic enabled",
|
||||
@ -177,7 +177,7 @@
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Ecobee thermostat on which to set active sensors."
|
||||
"description": "ecobee thermostat on which to set active sensors."
|
||||
},
|
||||
"preset_mode": {
|
||||
"name": "Climate Name",
|
||||
@ -203,12 +203,12 @@
|
||||
},
|
||||
"issues": {
|
||||
"migrate_aux_heat": {
|
||||
"title": "Migration of Ecobee set_aux_heat action",
|
||||
"title": "Migration of ecobee set_aux_heat action",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "The Ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
|
||||
"title": "Disable legacy Ecobee set_aux_heat action"
|
||||
"description": "The ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
|
||||
"title": "Disable legacy ecobee set_aux_heat action"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
|
||||
}
|
||||
|
@ -176,9 +176,9 @@
|
||||
"water_amount": {
|
||||
"name": "Water flow level",
|
||||
"state": {
|
||||
"high": "High",
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"medium": "[%key:common::state::medium%]",
|
||||
"ultrahigh": "Ultrahigh"
|
||||
}
|
||||
},
|
||||
|
@ -9,7 +9,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT]
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
18
homeassistant/components/eheimdigital/icons.json
Normal file
18
homeassistant/components/eheimdigital/icons.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"current_speed": {
|
||||
"default": "mdi:pump"
|
||||
},
|
||||
"service_hours": {
|
||||
"default": "mdi:wrench-clock"
|
||||
},
|
||||
"error_code": {
|
||||
"default": "mdi:alert-octagon",
|
||||
"state": {
|
||||
"no_error": "mdi:check-circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
114
homeassistant/components/eheimdigital/sensor.py
Normal file
114
homeassistant/components/eheimdigital/sensor.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""EHEIM Digital sensors."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.types import FilterErrorCode
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.components.sensor.const import SensorDeviceClass
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]):
|
||||
"""Class describing EHEIM Digital sensor entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], float | str | None]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalSensorDescription[EheimDigitalClassicVario], ...
|
||||
] = (
|
||||
EheimDigitalSensorDescription[EheimDigitalClassicVario](
|
||||
key="current_speed",
|
||||
translation_key="current_speed",
|
||||
value_fn=lambda device: device.current_speed,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
EheimDigitalSensorDescription[EheimDigitalClassicVario](
|
||||
key="service_hours",
|
||||
translation_key="service_hours",
|
||||
value_fn=lambda device: device.service_hours,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||
suggested_unit_of_measurement=UnitOfTime.DAYS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
EheimDigitalSensorDescription[EheimDigitalClassicVario](
|
||||
key="error_code",
|
||||
translation_key="error_code",
|
||||
value_fn=(
|
||||
lambda device: device.error_code.name.lower()
|
||||
if device.error_code is not None
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[name.lower() for name in FilterErrorCode._member_names_],
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: EheimDigitalConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the callbacks for the coordinator so lights can be added as devices are found."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def async_setup_device_entities(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the light entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalSensor[EheimDigitalDevice]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities += [
|
||||
EheimDigitalSensor[EheimDigitalClassicVario](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in CLASSICVARIO_DESCRIPTIONS
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
coordinator.add_platform_callback(async_setup_device_entities)
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalSensor(
|
||||
EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co]
|
||||
):
|
||||
"""Represent a EHEIM Digital sensor entity."""
|
||||
|
||||
entity_description: EheimDigitalSensorDescription[_DeviceT_co]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalSensorDescription[_DeviceT_co],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital number entity."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self._device_address}_{description.key}"
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
self._attr_native_value = self.entity_description.value_fn(self._device)
|
@ -46,6 +46,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"current_speed": {
|
||||
"name": "Current speed"
|
||||
},
|
||||
"service_hours": {
|
||||
"name": "Remaining hours until service"
|
||||
},
|
||||
"error_code": {
|
||||
"name": "Error code",
|
||||
"state": {
|
||||
"no_error": "No error",
|
||||
"rotor_stuck": "Rotor stuck",
|
||||
"air_in_filter": "Air in filter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,16 +66,19 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
]
|
||||
|
||||
for end_point in end_points:
|
||||
response = await envoy.request(end_point)
|
||||
fixture_data[end_point] = response.text.replace("\n", "").replace(
|
||||
serial, CLEAN_TEXT
|
||||
)
|
||||
fixture_data[f"{end_point}_log"] = json_dumps(
|
||||
{
|
||||
"headers": dict(response.headers.items()),
|
||||
"code": response.status_code,
|
||||
}
|
||||
)
|
||||
try:
|
||||
response = await envoy.request(end_point)
|
||||
fixture_data[end_point] = response.text.replace("\n", "").replace(
|
||||
serial, CLEAN_TEXT
|
||||
)
|
||||
fixture_data[f"{end_point}_log"] = json_dumps(
|
||||
{
|
||||
"headers": dict(response.headers.items()),
|
||||
"code": response.status_code,
|
||||
}
|
||||
)
|
||||
except EnvoyError as err:
|
||||
fixture_data[f"{end_point}_log"] = {"Error": repr(err)}
|
||||
return fixture_data
|
||||
|
||||
|
||||
@ -160,10 +163,7 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
fixture_data: dict[str, Any] = {}
|
||||
if entry.options.get(OPTION_DIAGNOSTICS_INCLUDE_FIXTURES, False):
|
||||
try:
|
||||
fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial)
|
||||
except EnvoyError as err:
|
||||
fixture_data["Error"] = repr(err)
|
||||
fixture_data = await _get_fixture_collection(envoy=envoy, serial=old_serial)
|
||||
|
||||
diagnostic_data: dict[str, Any] = {
|
||||
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.25.1"],
|
||||
"requirements": ["pyenphase==1.25.5"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.12.0"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.13.1"]
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ from aioesphomeapi import (
|
||||
APIConnectionError,
|
||||
APIVersion,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EncryptionHelloAPIError,
|
||||
EncryptionPlaintextAPIError,
|
||||
EntityInfo,
|
||||
HomeassistantServiceCall,
|
||||
InvalidAuthAPIError,
|
||||
@ -571,7 +571,7 @@ class ESPHomeManager:
|
||||
if isinstance(
|
||||
err,
|
||||
(
|
||||
EncryptionHelloAPIError,
|
||||
EncryptionPlaintextAPIError,
|
||||
RequiresEncryptionAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
InvalidAuthAPIError,
|
||||
|
@ -16,9 +16,9 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.8.0",
|
||||
"aioesphomeapi==29.9.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.12.0"
|
||||
"bleak-esphome==2.13.1"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
@ -152,7 +152,7 @@ class EvoZone(EvoChild, EvoClimateEntity):
|
||||
super().__init__(coordinator, evo_device)
|
||||
self._evo_id = evo_device.id
|
||||
|
||||
if evo_device.model.startswith("VisionProWifi"):
|
||||
if evo_device.id == evo_device.tcs.id:
|
||||
# this system does not have a distinct ID for the zone
|
||||
self._attr_unique_id = f"{evo_device.id}z"
|
||||
else:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.0.4"]
|
||||
"requirements": ["evohome-async==1.0.5"]
|
||||
}
|
||||
|
@ -14,9 +14,10 @@ from pyfibaro.fibaro_client import (
|
||||
)
|
||||
from pyfibaro.fibaro_data_helper import read_rooms
|
||||
from pyfibaro.fibaro_device import DeviceModel
|
||||
from pyfibaro.fibaro_device_manager import FibaroDeviceManager
|
||||
from pyfibaro.fibaro_info import InfoModel
|
||||
from pyfibaro.fibaro_scene import SceneModel
|
||||
from pyfibaro.fibaro_state_resolver import FibaroEvent, FibaroStateResolver
|
||||
from pyfibaro.fibaro_state_resolver import FibaroEvent
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, Platform
|
||||
@ -81,8 +82,8 @@ class FibaroController:
|
||||
self._client = fibaro_client
|
||||
self._fibaro_info = info
|
||||
|
||||
# Whether to import devices from plugins
|
||||
self._import_plugins = import_plugins
|
||||
# The fibaro device manager exposes higher level API to access fibaro devices
|
||||
self._fibaro_device_manager = FibaroDeviceManager(fibaro_client, import_plugins)
|
||||
# Mapping roomId to room object
|
||||
self._room_map = read_rooms(fibaro_client)
|
||||
self._device_map: dict[int, DeviceModel] # Mapping deviceId to device object
|
||||
@ -91,79 +92,30 @@ class FibaroController:
|
||||
) # List of devices by entity platform
|
||||
# All scenes
|
||||
self._scenes = self._client.read_scenes()
|
||||
self._callbacks: dict[int, list[Any]] = {} # Update value callbacks by deviceId
|
||||
# Event callbacks by device id
|
||||
self._event_callbacks: dict[int, list[Callable[[FibaroEvent], None]]] = {}
|
||||
# Unique serial number of the hub
|
||||
self.hub_serial = info.serial_number
|
||||
# Device infos by fibaro device id
|
||||
self._device_infos: dict[int, DeviceInfo] = {}
|
||||
self._read_devices()
|
||||
|
||||
def enable_state_handler(self) -> None:
|
||||
"""Start StateHandler thread for monitoring updates."""
|
||||
self._client.register_update_handler(self._on_state_change)
|
||||
def disconnect(self) -> None:
|
||||
"""Close push channel."""
|
||||
self._fibaro_device_manager.close()
|
||||
|
||||
def disable_state_handler(self) -> None:
|
||||
"""Stop StateHandler thread used for monitoring updates."""
|
||||
self._client.unregister_update_handler()
|
||||
|
||||
def _on_state_change(self, state: Any) -> None:
|
||||
"""Handle change report received from the HomeCenter."""
|
||||
callback_set = set()
|
||||
for change in state.get("changes", []):
|
||||
try:
|
||||
dev_id = change.pop("id")
|
||||
if dev_id not in self._device_map:
|
||||
continue
|
||||
device = self._device_map[dev_id]
|
||||
for property_name, value in change.items():
|
||||
if property_name == "log":
|
||||
if value and value != "transfer OK":
|
||||
_LOGGER.debug("LOG %s: %s", device.friendly_name, value)
|
||||
continue
|
||||
if property_name == "logTemp":
|
||||
continue
|
||||
if property_name in device.properties:
|
||||
device.properties[property_name] = value
|
||||
_LOGGER.debug(
|
||||
"<- %s.%s = %s", device.ha_id, property_name, str(value)
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning("%s.%s not found", device.ha_id, property_name)
|
||||
if dev_id in self._callbacks:
|
||||
callback_set.add(dev_id)
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
for item in callback_set:
|
||||
for callback in self._callbacks[item]:
|
||||
callback()
|
||||
|
||||
resolver = FibaroStateResolver(state)
|
||||
for event in resolver.get_events():
|
||||
# event does not always have a fibaro id, therefore it is
|
||||
# essential that we first check for relevant event type
|
||||
if (
|
||||
event.event_type.lower() == "centralsceneevent"
|
||||
and event.fibaro_id in self._event_callbacks
|
||||
):
|
||||
for callback in self._event_callbacks[event.fibaro_id]:
|
||||
callback(event)
|
||||
|
||||
def register(self, device_id: int, callback: Any) -> None:
|
||||
def register(
|
||||
self, device_id: int, callback: Callable[[DeviceModel], None]
|
||||
) -> Callable[[], None]:
|
||||
"""Register device with a callback for updates."""
|
||||
device_callbacks = self._callbacks.setdefault(device_id, [])
|
||||
device_callbacks.append(callback)
|
||||
return self._fibaro_device_manager.add_change_listener(device_id, callback)
|
||||
|
||||
def register_event(
|
||||
self, device_id: int, callback: Callable[[FibaroEvent], None]
|
||||
) -> None:
|
||||
) -> Callable[[], None]:
|
||||
"""Register device with a callback for central scene events.
|
||||
|
||||
The callback receives one parameter with the event.
|
||||
"""
|
||||
device_callbacks = self._event_callbacks.setdefault(device_id, [])
|
||||
device_callbacks.append(callback)
|
||||
return self._fibaro_device_manager.add_event_listener(device_id, callback)
|
||||
|
||||
def get_children(self, device_id: int) -> list[DeviceModel]:
|
||||
"""Get a list of child devices."""
|
||||
@ -286,7 +238,7 @@ class FibaroController:
|
||||
|
||||
def _read_devices(self) -> None:
|
||||
"""Read and process the device list."""
|
||||
devices = self._client.read_devices()
|
||||
devices = self._fibaro_device_manager.get_devices()
|
||||
self._device_map = {}
|
||||
last_climate_parent = None
|
||||
last_endpoint = None
|
||||
@ -301,8 +253,8 @@ class FibaroController:
|
||||
device.ha_id = (
|
||||
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
|
||||
)
|
||||
if device.enabled and (not device.is_plugin or self._import_plugins):
|
||||
platform = self._map_device_to_platform(device)
|
||||
|
||||
platform = self._map_device_to_platform(device)
|
||||
if platform is None:
|
||||
continue
|
||||
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
|
||||
@ -392,8 +344,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> bo
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
controller.enable_state_handler()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -402,8 +352,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: FibaroConfigEntry) -> b
|
||||
_LOGGER.debug("Shutting down Fibaro connection")
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
entry.runtime_data.disable_state_handler()
|
||||
|
||||
entry.runtime_data.disconnect()
|
||||
return unload_ok
|
||||
|
||||
|
||||
|
@ -36,9 +36,13 @@ class FibaroEntity(Entity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
self.controller.register(self.fibaro_device.fibaro_id, self._update_callback)
|
||||
self.async_on_remove(
|
||||
self.controller.register(
|
||||
self.fibaro_device.fibaro_id, self._update_callback
|
||||
)
|
||||
)
|
||||
|
||||
def _update_callback(self) -> None:
|
||||
def _update_callback(self, fibaro_device: DeviceModel) -> None:
|
||||
"""Update the state."""
|
||||
self.schedule_update_ha_state(True)
|
||||
|
||||
|
@ -60,11 +60,16 @@ class FibaroEventEntity(FibaroEntity, EventEntity):
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Register event callback
|
||||
self.controller.register_event(
|
||||
self.fibaro_device.fibaro_id, self._event_callback
|
||||
self.async_on_remove(
|
||||
self.controller.register_event(
|
||||
self.fibaro_device.fibaro_id, self._event_callback
|
||||
)
|
||||
)
|
||||
|
||||
def _event_callback(self, event: FibaroEvent) -> None:
|
||||
if event.key_id == self._button:
|
||||
if (
|
||||
event.event_type.lower() == "centralsceneevent"
|
||||
and event.key_id == self._button
|
||||
):
|
||||
self._trigger_event(event.key_event_type)
|
||||
self.schedule_update_ha_state()
|
||||
|
@ -53,5 +53,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["flux_led"],
|
||||
"requirements": ["flux-led==1.1.3"]
|
||||
"requirements": ["flux-led==1.2.0"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["forecast-solar==4.0.0"]
|
||||
"requirements": ["forecast-solar==4.1.0"]
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzBinarySensorEntityDescription(
|
||||
|
@ -31,6 +31,9 @@ from .entity import FritzDeviceBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Set a sane value to avoid too many updates
|
||||
PARALLEL_UPDATES = 5
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzButtonDescription(ButtonEntityDescription):
|
||||
|
@ -22,6 +22,9 @@ from .entity import FritzDeviceBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -18,6 +18,9 @@ from .entity import FritzBoxBaseEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -14,9 +14,7 @@ rules:
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions:
|
||||
status: todo
|
||||
comment: include the proper docs snippet
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name:
|
||||
@ -31,15 +29,11 @@ rules:
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters:
|
||||
status: todo
|
||||
comment: add the proper configuration_basic block
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates:
|
||||
status: todo
|
||||
comment: not set at the moment, we use a coordinator
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
@ -50,7 +44,7 @@ rules:
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations:
|
||||
status: exempt
|
||||
|
@ -32,6 +32,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def _uptime_calculation(seconds_uptime: float, last_value: datetime | None) -> datetime:
|
||||
"""Calculate uptime with deviation."""
|
||||
|
@ -38,6 +38,9 @@ from .entity import FritzBoxBaseEntity, FritzDeviceBase
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Set a sane value to avoid too many updates
|
||||
PARALLEL_UPDATES = 5
|
||||
|
||||
|
||||
async def _async_deflection_entities_list(
|
||||
avm_wrapper: AvmWrapper, device_friendly_name: str
|
||||
|
@ -20,6 +20,9 @@ from .entity import FritzBoxBaseCoordinatorEntity, FritzEntityDescription
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Set a sane value to avoid too many updates
|
||||
PARALLEL_UPDATES = 5
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzUpdateEntityDescription(UpdateEntityDescription, FritzEntityDescription):
|
||||
|
@ -137,6 +137,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
key="battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=lambda device: device.battery_level is not None,
|
||||
native_value=lambda device: device.battery_level,
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250328.0"]
|
||||
"requirements": ["home-assistant-frontend==20250404.0"]
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"common": {
|
||||
"data_description_password": "The Remote Admin password from the Fully Kiosk Browser app settings.",
|
||||
"data_description_ssl": "Is the Fully Kiosk app configured to require SSL for the connection?",
|
||||
"data_description_verify_ssl": "Should SSL certificartes be verified? This should be off for self-signed certificates."
|
||||
"data_description_verify_ssl": "Should SSL certificates be verified? This should be off for self-signed certificates."
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
|
@ -79,9 +79,9 @@
|
||||
"state": {
|
||||
"no_data": "No data",
|
||||
"too_low": "Too low",
|
||||
"low": "Low",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"perfect": "Perfect",
|
||||
"high": "High",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"too_high": "Too high"
|
||||
}
|
||||
},
|
||||
@ -90,9 +90,9 @@
|
||||
"state": {
|
||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||
}
|
||||
},
|
||||
@ -101,9 +101,9 @@
|
||||
"state": {
|
||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||
}
|
||||
},
|
||||
@ -112,9 +112,9 @@
|
||||
"state": {
|
||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||
}
|
||||
},
|
||||
@ -123,9 +123,9 @@
|
||||
"state": {
|
||||
"no_data": "[%key:component::fyta::entity::sensor::temperature_status::state::no_data%]",
|
||||
"too_low": "[%key:component::fyta::entity::sensor::temperature_status::state::too_low%]",
|
||||
"low": "[%key:component::fyta::entity::sensor::temperature_status::state::low%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"perfect": "[%key:component::fyta::entity::sensor::temperature_status::state::perfect%]",
|
||||
"high": "[%key:component::fyta::entity::sensor::temperature_status::state::high%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"too_high": "[%key:component::fyta::entity::sensor::temperature_status::state::too_high%]"
|
||||
}
|
||||
},
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"]
|
||||
}
|
||||
|
@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
if not (
|
||||
user_input.get(CONF_LLM_HASS_API, "none") != "none"
|
||||
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
|
||||
):
|
||||
# Don't allow to save options that enable the Google Seearch tool with an Assist API
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
|
||||
|
||||
# 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[CONF_LLM_HASS_API],
|
||||
}
|
||||
options = user_input
|
||||
|
||||
schema = await google_generative_ai_config_option_schema(
|
||||
self.hass, options, self._genai_client
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(schema),
|
||||
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||
)
|
||||
|
||||
|
||||
|
@ -55,6 +55,10 @@ from .const import (
|
||||
# Max number of back and forth with the LLM to generate a response
|
||||
MAX_TOOL_ITERATIONS = 10
|
||||
|
||||
ERROR_GETTING_RESPONSE = (
|
||||
"Sorry, I had a problem getting a response from Google Generative AI."
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -429,6 +433,12 @@ class GoogleGenerativeAIConversationEntity(
|
||||
raise HomeAssistantError(
|
||||
f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}"
|
||||
)
|
||||
if not chat_response.candidates:
|
||||
LOGGER.error(
|
||||
"No candidates found in the response: %s",
|
||||
chat_response,
|
||||
)
|
||||
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||
|
||||
except (
|
||||
APIError,
|
||||
@ -452,9 +462,7 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
response_parts = chat_response.candidates[0].content.parts
|
||||
if not response_parts:
|
||||
raise HomeAssistantError(
|
||||
"Sorry, I had a problem getting a response from Google Generative AI."
|
||||
)
|
||||
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||
content = " ".join(
|
||||
[part.text.strip() for part in response_parts if part.text]
|
||||
)
|
||||
|
@ -40,9 +40,13 @@
|
||||
"enable_google_search_tool": "Enable Google Search tool"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template."
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -24,8 +24,8 @@
|
||||
"fix_menu": {
|
||||
"description": "Add-on {addon} is set to start at boot but failed to start. Usually this occurs when the configuration is incorrect or the same port is used in multiple add-ons. Check the configuration as well as logs for {addon} and Supervisor.\n\nUse Start to try again or Disable to turn off the start at boot option.",
|
||||
"menu_options": {
|
||||
"addon_execute_start": "Start",
|
||||
"addon_disable_boot": "Disable"
|
||||
"addon_execute_start": "[%key:common::action::start%]",
|
||||
"addon_disable_boot": "[%key:common::action::disable%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -265,6 +265,11 @@
|
||||
"version_latest": {
|
||||
"name": "Newest version"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"update": {
|
||||
"name": "[%key:component::update::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -39,7 +39,7 @@ from .entity import (
|
||||
from .update_helper import update_addon, update_core
|
||||
|
||||
ENTITY_DESCRIPTION = UpdateEntityDescription(
|
||||
name="Update",
|
||||
translation_key="update",
|
||||
key=ATTR_VERSION_LATEST,
|
||||
)
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_DESTINATION_POSITION = "destination_position"
|
||||
ATTR_QUEUE_IDS = "queue_ids"
|
||||
DOMAIN = "heos"
|
||||
ENTRY_TITLE = "HEOS System"
|
||||
@ -9,6 +10,7 @@ SERVICE_GET_QUEUE = "get_queue"
|
||||
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
||||
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
||||
SERVICE_MOVE_QUEUE_ITEM = "move_queue_item"
|
||||
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
|
||||
SERVICE_SIGN_IN = "sign_in"
|
||||
SERVICE_SIGN_OUT = "sign_out"
|
||||
|
@ -6,6 +6,9 @@
|
||||
"remove_from_queue": {
|
||||
"service": "mdi:playlist-remove"
|
||||
},
|
||||
"move_queue_item": {
|
||||
"service": "mdi:playlist-edit"
|
||||
},
|
||||
"group_volume_set": {
|
||||
"service": "mdi:volume-medium"
|
||||
},
|
||||
|
@ -479,6 +479,13 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
"""Remove items from the queue."""
|
||||
await self._player.remove_from_queue(queue_ids)
|
||||
|
||||
@catch_action_error("move queue item")
|
||||
async def async_move_queue_item(
|
||||
self, queue_ids: list[int], destination_position: int
|
||||
) -> None:
|
||||
"""Move items in the queue."""
|
||||
await self._player.move_queue_item(queue_ids, destination_position)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if the device is available."""
|
||||
|
@ -19,6 +19,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.helpers.typing import VolDictType, VolSchemaType
|
||||
|
||||
from .const import (
|
||||
ATTR_DESTINATION_POSITION,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_QUEUE_IDS,
|
||||
ATTR_USERNAME,
|
||||
@ -27,6 +28,7 @@ from .const import (
|
||||
SERVICE_GROUP_VOLUME_DOWN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
SERVICE_GROUP_VOLUME_UP,
|
||||
SERVICE_MOVE_QUEUE_ITEM,
|
||||
SERVICE_REMOVE_FROM_QUEUE,
|
||||
SERVICE_SIGN_IN,
|
||||
SERVICE_SIGN_OUT,
|
||||
@ -87,6 +89,16 @@ REMOVE_FROM_QUEUE_SCHEMA: Final[VolDictType] = {
|
||||
GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = {
|
||||
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float
|
||||
}
|
||||
MOVE_QEUEUE_ITEM_SCHEMA: Final[VolDictType] = {
|
||||
vol.Required(ATTR_QUEUE_IDS): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.All(vol.Coerce(int), vol.Range(min=1, max=1000))],
|
||||
vol.Unique(),
|
||||
),
|
||||
vol.Required(ATTR_DESTINATION_POSITION): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=1000)
|
||||
),
|
||||
}
|
||||
|
||||
MEDIA_PLAYER_ENTITY_SERVICES: Final = (
|
||||
# Player queue services
|
||||
@ -96,6 +108,9 @@ MEDIA_PLAYER_ENTITY_SERVICES: Final = (
|
||||
EntityServiceDescription(
|
||||
SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA
|
||||
),
|
||||
EntityServiceDescription(
|
||||
SERVICE_MOVE_QUEUE_ITEM, "async_move_queue_item", MOVE_QEUEUE_ITEM_SCHEMA
|
||||
),
|
||||
# Group volume services
|
||||
EntityServiceDescription(
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
|
@ -17,6 +17,26 @@ remove_from_queue:
|
||||
multiple: true
|
||||
type: number
|
||||
|
||||
move_queue_item:
|
||||
target:
|
||||
entity:
|
||||
integration: heos
|
||||
domain: media_player
|
||||
fields:
|
||||
queue_ids:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
type: number
|
||||
destination_position:
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 1000
|
||||
step: 1
|
||||
|
||||
group_volume_set:
|
||||
target:
|
||||
entity:
|
||||
|
@ -100,6 +100,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"move_queue_item": {
|
||||
"name": "Move queue item",
|
||||
"description": "Move one or more items within the play queue.",
|
||||
"fields": {
|
||||
"queue_ids": {
|
||||
"name": "Queue IDs",
|
||||
"description": "The IDs (indexes) of the items in the queue to move."
|
||||
},
|
||||
"destination_position": {
|
||||
"name": "Destination position",
|
||||
"description": "The position index in the queue to move the items to."
|
||||
}
|
||||
}
|
||||
},
|
||||
"group_volume_down": {
|
||||
"name": "Turn down group volume",
|
||||
"description": "Turns down the group volume."
|
||||
|
@ -34,9 +34,9 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_username": "Failed to sign into Hive. Your email address is not recognised.",
|
||||
"invalid_password": "Failed to sign into Hive. Incorrect password, please try again.",
|
||||
"invalid_code": "Failed to sign into Hive. Your two-factor authentication code was incorrect.",
|
||||
"invalid_username": "Failed to sign in to Hive. Your email address is not recognised.",
|
||||
"invalid_password": "Failed to sign in to Hive. Incorrect password, please try again.",
|
||||
"invalid_code": "Failed to sign in to Hive. Your two-factor authentication code was incorrect.",
|
||||
"no_internet_available": "An Internet connection is required to connect to Hive.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
|
@ -5,37 +5,18 @@ from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, StatusKey
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .const import (
|
||||
BSH_DOOR_STATE_CLOSED,
|
||||
BSH_DOOR_STATE_LOCKED,
|
||||
BSH_DOOR_STATE_OPEN,
|
||||
DOMAIN,
|
||||
REFRIGERATION_STATUS_DOOR_CLOSED,
|
||||
REFRIGERATION_STATUS_DOOR_OPEN,
|
||||
)
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .const import REFRIGERATION_STATUS_DOOR_CLOSED, REFRIGERATION_STATUS_DOOR_OPEN
|
||||
from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@ -173,8 +154,6 @@ def _get_entities_for_appliance(
|
||||
for description in BINARY_SENSORS
|
||||
if description.key in appliance.status
|
||||
)
|
||||
if StatusKey.BSH_COMMON_DOOR_STATE in appliance.status:
|
||||
entities.append(HomeConnectDoorBinarySensor(entry.runtime_data, appliance))
|
||||
return entities
|
||||
|
||||
|
||||
@ -220,83 +199,3 @@ class HomeConnectConnectivityBinarySensor(HomeConnectEntity, BinarySensorEntity)
|
||||
def available(self) -> bool:
|
||||
"""Return the availability."""
|
||||
return self.coordinator.last_update_success
|
||||
|
||||
|
||||
class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
"""Binary sensor for Home Connect Generic Door."""
|
||||
|
||||
_attr_has_entity_name = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HomeConnectCoordinator,
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=StatusKey.BSH_COMMON_DOOR_STATE,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
boolean_map={
|
||||
BSH_DOOR_STATE_CLOSED: False,
|
||||
BSH_DOOR_STATE_LOCKED: False,
|
||||
BSH_DOOR_STATE_OPEN: True,
|
||||
},
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-Door"
|
||||
self._attr_name = f"{appliance.info.name} Door"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
automations = automations_with_entity(self.hass, self.entity_id)
|
||||
scripts = scripts_with_entity(self.hass, self.entity_id)
|
||||
items = automations + scripts
|
||||
if not items:
|
||||
return
|
||||
|
||||
entity_reg: er.EntityRegistry = er.async_get(self.hass)
|
||||
entity_automations = [
|
||||
automation_entity
|
||||
for automation_id in automations
|
||||
if (automation_entity := entity_reg.async_get(automation_id))
|
||||
]
|
||||
entity_scripts = [
|
||||
script_entity
|
||||
for script_id in scripts
|
||||
if (script_entity := entity_reg.async_get(script_id))
|
||||
]
|
||||
|
||||
items_list = [
|
||||
f"- [{item.original_name}](/config/automation/edit/{item.unique_id})"
|
||||
for item in entity_automations
|
||||
] + [
|
||||
f"- [{item.original_name}](/config/script/edit/{item.unique_id})"
|
||||
for item in entity_scripts
|
||||
]
|
||||
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_binary_common_door_sensor_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.5.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_binary_common_door_sensor",
|
||||
translation_placeholders={
|
||||
"entity": self.entity_id,
|
||||
"items": "\n".join(items_list),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed from hass."""
|
||||
await super().async_will_remove_from_hass()
|
||||
async_delete_issue(
|
||||
self.hass, DOMAIN, f"deprecated_binary_common_door_sensor_{self.entity_id}"
|
||||
)
|
||||
|
@ -74,6 +74,19 @@ class HomeConnectApplianceData:
|
||||
self.settings.update(other.settings)
|
||||
self.status.update(other.status)
|
||||
|
||||
@classmethod
|
||||
def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData:
|
||||
"""Return empty data."""
|
||||
return cls(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectCoordinator(
|
||||
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
|
||||
@ -362,15 +375,7 @@ class HomeConnectCoordinator(
|
||||
model=appliance.vib,
|
||||
)
|
||||
if appliance.ha_id not in self.data:
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance)
|
||||
else:
|
||||
self.data[appliance.ha_id].info.connected = appliance.connected
|
||||
old_appliances.remove(appliance.ha_id)
|
||||
@ -406,6 +411,15 @@ class HomeConnectCoordinator(
|
||||
name=appliance.name,
|
||||
model=appliance.vib,
|
||||
)
|
||||
if not appliance.connected:
|
||||
_LOGGER.debug(
|
||||
"Appliance %s is not connected, skipping data fetch",
|
||||
appliance.ha_id,
|
||||
)
|
||||
if appliance_data_to_update:
|
||||
appliance_data_to_update.info.connected = False
|
||||
return appliance_data_to_update
|
||||
return HomeConnectApplianceData.empty(appliance)
|
||||
try:
|
||||
settings = {
|
||||
setting.key: setting
|
||||
|
@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.16.3"],
|
||||
"requirements": ["aiohomeconnect==0.17.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -132,17 +132,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_binary_common_door_sensor": {
|
||||
"title": "Deprecated binary door sensor detected in some automations or scripts",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]",
|
||||
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_command_actions": {
|
||||
"title": "The command related actions are deprecated in favor of the new buttons",
|
||||
"fix_flow": {
|
||||
@ -487,9 +476,9 @@
|
||||
},
|
||||
"warming_level": {
|
||||
"options": {
|
||||
"cooking_oven_enum_type_warming_level_low": "Low",
|
||||
"cooking_oven_enum_type_warming_level_medium": "Medium",
|
||||
"cooking_oven_enum_type_warming_level_high": "High"
|
||||
"cooking_oven_enum_type_warming_level_low": "[%key:common::state::low%]",
|
||||
"cooking_oven_enum_type_warming_level_medium": "[%key:common::state::medium%]",
|
||||
"cooking_oven_enum_type_warming_level_high": "[%key:common::state::high%]"
|
||||
}
|
||||
},
|
||||
"washer_temperature": {
|
||||
@ -522,9 +511,9 @@
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "High"
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:common::state::low%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:common::state::medium%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:common::state::high%]"
|
||||
}
|
||||
},
|
||||
"vario_perfect": {
|
||||
@ -1468,9 +1457,9 @@
|
||||
"warming_level": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_oven_option_warming_level::name%]",
|
||||
"state": {
|
||||
"cooking_oven_enum_type_warming_level_low": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_low%]",
|
||||
"cooking_oven_enum_type_warming_level_medium": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_medium%]",
|
||||
"cooking_oven_enum_type_warming_level_high": "[%key:component::home_connect::selector::warming_level::options::cooking_oven_enum_type_warming_level_high%]"
|
||||
"cooking_oven_enum_type_warming_level_low": "[%key:common::state::low%]",
|
||||
"cooking_oven_enum_type_warming_level_medium": "[%key:common::state::medium%]",
|
||||
"cooking_oven_enum_type_warming_level_high": "[%key:common::state::high%]"
|
||||
}
|
||||
},
|
||||
"washer_temperature": {
|
||||
@ -1505,9 +1494,9 @@
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_high%]"
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:common::state::low%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:common::state::medium%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:common::state::high%]"
|
||||
}
|
||||
},
|
||||
"vario_perfect": {
|
||||
|
@ -33,6 +33,7 @@ from .util import (
|
||||
OwningIntegration,
|
||||
get_otbr_addon_manager,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
guess_firmware_info,
|
||||
guess_hardware_owners,
|
||||
probe_silabs_firmware_info,
|
||||
)
|
||||
@ -511,6 +512,16 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm a discovery."""
|
||||
assert self._device is not None
|
||||
fw_info = await guess_firmware_info(self.hass, self._device)
|
||||
|
||||
# If our guess for the firmware type is actually running, we can save the user
|
||||
# an unnecessary confirmation and silently confirm the flow
|
||||
for owner in fw_info.owners:
|
||||
if await owner.is_running(self.hass):
|
||||
self._probed_firmware_info = fw_info
|
||||
return self._async_flow_finished()
|
||||
|
||||
return await self.async_step_pick_firmware()
|
||||
|
||||
|
||||
|
@ -95,8 +95,7 @@ class BaseFirmwareUpdateEntity(
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||
)
|
||||
# Until this entity can be associated with a device, we must manually name it
|
||||
_attr_has_entity_name = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -195,10 +194,6 @@ class BaseFirmwareUpdateEntity(
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
|
||||
# This entity is not currently associated with a device so we must manually
|
||||
# give it a name
|
||||
self._attr_name = f"{self._config_entry.title} Update"
|
||||
self._attr_title = self.entity_description.firmware_name or "Unknown"
|
||||
|
||||
if (
|
||||
|
@ -3,19 +3,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
async_register_port_event_callback,
|
||||
scan_serial_ports,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DESCRIPTION, DEVICE, FIRMWARE, FIRMWARE_VERSION, PRODUCT
|
||||
from .const import (
|
||||
DESCRIPTION,
|
||||
DEVICE,
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
PID,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
VID,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the ZBT-1 integration."""
|
||||
|
||||
@callback
|
||||
def async_port_event_callback(
|
||||
added: set[USBDevice], removed: set[USBDevice]
|
||||
) -> None:
|
||||
"""Handle USB port events."""
|
||||
current_entries_by_path = {
|
||||
entry.data[DEVICE]: entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
}
|
||||
|
||||
for device in added | removed:
|
||||
path = device.device
|
||||
entry = current_entries_by_path.get(path)
|
||||
|
||||
if entry is not None:
|
||||
_LOGGER.debug(
|
||||
"Device %r has changed state, reloading config entry %s",
|
||||
path,
|
||||
entry,
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async_register_port_event_callback(hass, async_port_event_callback)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant SkyConnect config entry."""
|
||||
|
||||
# Postpone loading the config entry if the device is missing
|
||||
device_path = entry.data[DEVICE]
|
||||
if not await hass.async_add_executor_job(os.path.exists, device_path):
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -29,7 +91,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
"""Migrate old entry."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s:%s", config_entry.version, config_entry.minor_version
|
||||
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
|
||||
)
|
||||
|
||||
if config_entry.version == 1:
|
||||
@ -64,6 +126,43 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
if config_entry.minor_version == 3:
|
||||
# Old SkyConnect config entries were missing keys
|
||||
if any(
|
||||
key not in config_entry.data
|
||||
for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER)
|
||||
):
|
||||
serial_ports = await hass.async_add_executor_job(scan_serial_ports)
|
||||
serial_ports_info = {port.device: port for port in serial_ports}
|
||||
device = config_entry.data[DEVICE]
|
||||
|
||||
if not (usb_info := serial_ports_info.get(device)):
|
||||
raise HomeAssistantError(
|
||||
f"USB device {device} is missing, cannot migrate"
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
VID: usb_info.vid,
|
||||
PID: usb_info.pid,
|
||||
MANUFACTURER: usb_info.manufacturer,
|
||||
PRODUCT: usb_info.description,
|
||||
DESCRIPTION: usb_info.description,
|
||||
SERIAL_NUMBER: usb_info.serial_number,
|
||||
},
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
else:
|
||||
# Existing entries are migrated by just incrementing the version
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
config_entry.version,
|
||||
|
@ -81,7 +81,7 @@ class HomeAssistantSkyConnectConfigFlow(
|
||||
"""Handle a config flow for Home Assistant SkyConnect."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 3
|
||||
MINOR_VERSION = 4
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize the config flow."""
|
||||
|
@ -5,17 +5,21 @@ from __future__ import annotations
|
||||
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .config_flow import HomeAssistantSkyConnectConfigFlow
|
||||
from .const import DOMAIN
|
||||
from .util import get_hardware_variant
|
||||
|
||||
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
|
||||
EXPECTED_ENTRY_VERSION = (
|
||||
HomeAssistantSkyConnectConfigFlow.VERSION,
|
||||
HomeAssistantSkyConnectConfigFlow.MINOR_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
"""Return board info."""
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
return [
|
||||
HardwareInfo(
|
||||
board=None,
|
||||
@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
url=DOCUMENTATION_URL,
|
||||
)
|
||||
for entry in entries
|
||||
# Ignore unmigrated config entries in the hardware page
|
||||
if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION
|
||||
]
|
||||
|
@ -195,5 +195,10 @@
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,7 +168,6 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""SkyConnect firmware update entity."""
|
||||
|
||||
bootloader_reset_type = None
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -152,7 +152,7 @@
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"firmware": {
|
||||
"radio_firmware": {
|
||||
"name": "Radio firmware"
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
] = {
|
||||
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -55,6 +56,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -65,7 +67,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -76,7 +79,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -88,6 +92,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -168,7 +173,6 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Yellow firmware update entity."""
|
||||
|
||||
bootloader_reset_type = "yellow" # Triggers a GPIO reset
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -17,6 +17,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
|
200
homeassistant/components/homee/climate.py
Normal file
200
homeassistant/components/homee/climate.py
Normal file
@ -0,0 +1,200 @@
|
||||
"""The Homee climate platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType, NodeProfile
|
||||
from pyHomee.model import HomeeNode
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRESET_BOOST,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .const import CLIMATE_PROFILES, DOMAIN, HOMEE_UNIT_TO_HA_UNIT, PRESET_MANUAL
|
||||
from .entity import HomeeNodeEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
ROOM_THERMOSTATS = {
|
||||
NodeProfile.ROOM_THERMOSTAT,
|
||||
NodeProfile.ROOM_THERMOSTAT_WITH_HUMIDITY_SENSOR,
|
||||
NodeProfile.WIFI_ROOM_THERMOSTAT,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the climate component."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeClimate(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if node.profile in CLIMATE_PROFILES
|
||||
)
|
||||
|
||||
|
||||
class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
"""Representation of a Homee climate entity."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_translation_key = DOMAIN
|
||||
|
||||
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
|
||||
"""Initialize a Homee climate entity."""
|
||||
super().__init__(node, entry)
|
||||
|
||||
(
|
||||
self._attr_supported_features,
|
||||
self._attr_hvac_modes,
|
||||
self._attr_preset_modes,
|
||||
) = get_climate_features(self._node)
|
||||
|
||||
self._target_temp = self._node.get_attribute_by_type(
|
||||
AttributeType.TARGET_TEMPERATURE
|
||||
)
|
||||
assert self._target_temp is not None
|
||||
self._attr_temperature_unit = str(HOMEE_UNIT_TO_HA_UNIT[self._target_temp.unit])
|
||||
self._attr_target_temperature_step = self._target_temp.step_value
|
||||
self._attr_unique_id = f"{self._attr_unique_id}-{self._target_temp.id}"
|
||||
|
||||
self._heating_mode = self._node.get_attribute_by_type(
|
||||
AttributeType.HEATING_MODE
|
||||
)
|
||||
self._temperature = self._node.get_attribute_by_type(AttributeType.TEMPERATURE)
|
||||
self._valve_position = self._node.get_attribute_by_type(
|
||||
AttributeType.CURRENT_VALVE_POSITION
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return the hvac operation mode."""
|
||||
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
|
||||
self._heating_mode is not None
|
||||
):
|
||||
if self._heating_mode.current_value == 0:
|
||||
return HVACMode.OFF
|
||||
|
||||
return HVACMode.HEAT
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return the hvac action."""
|
||||
if self._heating_mode is not None and self._heating_mode.current_value == 0:
|
||||
return HVACAction.OFF
|
||||
|
||||
if (
|
||||
self._valve_position is not None and self._valve_position.current_value == 0
|
||||
) or (
|
||||
self._temperature is not None
|
||||
and self._temperature.current_value >= self.target_temperature
|
||||
):
|
||||
return HVACAction.IDLE
|
||||
|
||||
return HVACAction.HEATING
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
"""Return the present preset mode."""
|
||||
if (
|
||||
ClimateEntityFeature.PRESET_MODE in self.supported_features
|
||||
and self._heating_mode is not None
|
||||
and self._heating_mode.current_value > 0
|
||||
):
|
||||
assert self._attr_preset_modes is not None
|
||||
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
|
||||
|
||||
return PRESET_NONE
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if self._temperature is not None:
|
||||
return self._temperature.current_value
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Return the temperature we try to reach."""
|
||||
assert self._target_temp is not None
|
||||
return self._target_temp.current_value
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the lowest settable target temperature."""
|
||||
assert self._target_temp is not None
|
||||
return self._target_temp.minimum
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the lowest settable target temperature."""
|
||||
assert self._target_temp is not None
|
||||
return self._target_temp.maximum
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
# Currently only HEAT and OFF are supported.
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
assert self._heating_mode is not None and self._attr_preset_modes is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
assert self._target_temp is not None
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
await self.async_set_homee_value(
|
||||
self._target_temp, kwargs[ATTR_TEMPERATURE]
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(self._heating_mode, 1)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(self._heating_mode, 0)
|
||||
|
||||
|
||||
def get_climate_features(
|
||||
node: HomeeNode,
|
||||
) -> tuple[ClimateEntityFeature, list[HVACMode], list[str] | None]:
|
||||
"""Determine supported climate features of a node based on the available attributes."""
|
||||
features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
hvac_modes = [HVACMode.HEAT]
|
||||
preset_modes: list[str] = []
|
||||
|
||||
if (
|
||||
attribute := node.get_attribute_by_type(AttributeType.HEATING_MODE)
|
||||
) is not None:
|
||||
features |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||
hvac_modes.append(HVACMode.OFF)
|
||||
|
||||
if attribute.maximum > 1:
|
||||
# Node supports more modes than off and heating.
|
||||
features |= ClimateEntityFeature.PRESET_MODE
|
||||
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
|
||||
|
||||
if len(preset_modes) > 0:
|
||||
preset_modes.insert(0, PRESET_NONE)
|
||||
return (features, hvac_modes, preset_modes if len(preset_modes) > 0 else None)
|
@ -95,3 +95,6 @@ LIGHT_PROFILES = [
|
||||
NodeProfile.WIFI_DIMMABLE_LIGHT,
|
||||
NodeProfile.WIFI_ON_OFF_DIMMABLE_METERING_SWITCH,
|
||||
]
|
||||
|
||||
# Climate Presets
|
||||
PRESET_MANUAL = "manual"
|
||||
|
@ -1,5 +1,16 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"homee": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"manual": "mdi:hand-back-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"brightness": {
|
||||
"default": "mdi:brightness-5"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user