mirror of
https://github.com/home-assistant/core.git
synced 2025-04-20 07:18:00 +00:00
Compare commits
99 Commits
dev
...
2025.4.0b1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
04f5315ab2 | ||
![]() |
7f9e4ba39e | ||
![]() |
06aaf188ea | ||
![]() |
627f994872 | ||
![]() |
9e81ec5aae | ||
![]() |
69753fca1d | ||
![]() |
7773cc121e | ||
![]() |
3aa56936ad | ||
![]() |
e66416c23d | ||
![]() |
a592feae3d | ||
![]() |
fc0d71e891 | ||
![]() |
d4640f1d24 | ||
![]() |
6fe158836e | ||
![]() |
629c0087f4 | ||
![]() |
363bd75129 | ||
![]() |
7592d350a8 | ||
![]() |
8ac8401b4e | ||
![]() |
eed075dbfa | ||
![]() |
23dbdedfb6 | ||
![]() |
85ad29e28e | ||
![]() |
35fc81b038 | ||
![]() |
5d45b84cd2 | ||
![]() |
7766649304 | ||
![]() |
07e9020dfa | ||
![]() |
f504a759e0 | ||
![]() |
9927de4801 | ||
![]() |
1244fc4682 | ||
![]() |
e77a1b12f7 | ||
![]() |
5459daaa10 | ||
![]() |
400131df78 | ||
![]() |
28e1843ff9 | ||
![]() |
df777318d1 | ||
![]() |
6ad5e9e89c | ||
![]() |
a0bd8deee9 | ||
![]() |
405cbd6a00 | ||
![]() |
3e0eb5ab2c | ||
![]() |
fad75a70b6 | ||
![]() |
d9720283df | ||
![]() |
14eed1778b | ||
![]() |
049aaa7e8b | ||
![]() |
35717e8216 | ||
![]() |
2a081abc18 | ||
![]() |
b7f29c7358 | ||
![]() |
3bb6373df5 | ||
![]() |
e1b4edec50 | ||
![]() |
147bee57e1 | ||
![]() |
fcdaea64da | ||
![]() |
d1512d46be | ||
![]() |
0be7db6270 | ||
![]() |
2af0282725 | ||
![]() |
ff458c8417 | ||
![]() |
cc93152ff0 | ||
![]() |
9965f01609 | ||
![]() |
e9c76ce694 | ||
![]() |
58ab7d350d | ||
![]() |
e4d6e20ebd | ||
![]() |
45e273897a | ||
![]() |
d9ec7142d7 | ||
![]() |
e162499267 | ||
![]() |
67f21429e3 | ||
![]() |
a0563f06c9 | ||
![]() |
e7c4fdc8bb | ||
![]() |
c490e350bc | ||
![]() |
e11409ef99 | ||
![]() |
5c8e415a76 | ||
![]() |
e795fb9497 | ||
![]() |
d0afabb85c | ||
![]() |
4f3e8e9b94 | ||
![]() |
46c1cbbc9c | ||
![]() |
8d9a4ea278 | ||
![]() |
22c83e2393 | ||
![]() |
c83a75f6f9 | ||
![]() |
841c727112 | ||
![]() |
d8c9655bfd | ||
![]() |
942ed89cc4 | ||
![]() |
a1fe6b9cf3 | ||
![]() |
2567181cc2 | ||
![]() |
028e4f6029 | ||
![]() |
b82e1a9bef | ||
![]() |
438f226c31 | ||
![]() |
2f139e3cb1 | ||
![]() |
5d75e96fbf | ||
![]() |
dcf2ec5c37 | ||
![]() |
2431e1ba98 | ||
![]() |
4ead108c15 | ||
![]() |
ec8363fa49 | ||
![]() |
e7ff0a3f8b | ||
![]() |
f4c0eb4189 | ||
![]() |
b1ee5a76e1 | ||
![]() |
6b9e8c301b | ||
![]() |
89c3266c7e | ||
![]() |
cff0a632e8 | ||
![]() |
e04d8557ae | ||
![]() |
ca6286f241 | ||
![]() |
35bcc9d5af | ||
![]() |
25b45ce867 | ||
![]() |
d568209bd5 | ||
![]() |
8a43e8af9e | ||
![]() |
785e5b2c16 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1480,8 +1480,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/sunweg/ @rokam
|
||||
/tests/components/sunweg/ @rokam
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
|
@ -859,14 +859,8 @@ async def _async_set_up_integrations(
|
||||
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
||||
hass, config
|
||||
)
|
||||
# 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
|
||||
all_domains = set(all_integrations)
|
||||
domains = set(integrations)
|
||||
|
||||
_LOGGER.info(
|
||||
"Domains to be set up: %s | %s",
|
||||
@ -874,8 +868,6 @@ 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)
|
||||
@ -908,12 +900,24 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered = {
|
||||
dep
|
||||
for domain in stage_domains
|
||||
for dep in integrations_after_dependencies[domain]
|
||||
for dep in all_integrations[domain].all_dependencies
|
||||
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",
|
||||
@ -924,6 +928,8 @@ 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/bosch.json
Normal file
5
homeassistant/brands/bosch.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "bosch",
|
||||
"name": "Bosch",
|
||||
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
|
||||
}
|
@ -8,7 +8,7 @@ from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from pyairnow import WebServiceAPI
|
||||
from pyairnow.conv import aqi_to_concentration
|
||||
from pyairnow.errors import AirNowError
|
||||
from pyairnow.errors import AirNowError, InvalidJsonError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
distance=self.distance,
|
||||
)
|
||||
|
||||
except (AirNowError, ClientConnectorError) as error:
|
||||
except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
if not obs:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"geography_by_coords": {
|
||||
"title": "Configure a Geography",
|
||||
"title": "Configure a geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
@ -56,12 +56,12 @@
|
||||
"sensor": {
|
||||
"pollutant_label": {
|
||||
"state": {
|
||||
"co": "Carbon Monoxide",
|
||||
"n2": "Nitrogen Dioxide",
|
||||
"co": "Carbon monoxide",
|
||||
"n2": "Nitrogen dioxide",
|
||||
"o3": "Ozone",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Sulfur Dioxide"
|
||||
"s2": "Sulfur dioxide"
|
||||
}
|
||||
},
|
||||
"pollutant_level": {
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Base class for assist satellite entities."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@ -15,6 +17,8 @@ from .const import (
|
||||
CONNECTION_TEST_DATA,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
PREANNOUNCE_FILENAME,
|
||||
PREANNOUNCE_URL,
|
||||
AssistSatelliteEntityFeature,
|
||||
)
|
||||
from .entity import (
|
||||
@ -56,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("message", "media_id"),
|
||||
@ -71,7 +75,7 @@ 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"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
),
|
||||
@ -84,6 +88,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_register_websocket_api(hass)
|
||||
hass.http.register_view(ConnectionTestView())
|
||||
|
||||
# Default preannounce sound
|
||||
await hass.http.async_register_static_paths(
|
||||
[
|
||||
StaticPathConfig(
|
||||
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
|
||||
f"{DOMAIN}_connection_tests"
|
||||
)
|
||||
|
||||
PREANNOUNCE_FILENAME = "preannounce.mp3"
|
||||
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
|
||||
|
||||
|
||||
class AssistSatelliteEntityFeature(IntFlag):
|
||||
"""Supported features of Assist satellite entity."""
|
||||
|
@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import chat_session, entity
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
|
||||
from .const import AssistSatelliteEntityFeature
|
||||
from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
|
||||
from .errors import AssistSatelliteError, SatelliteBusyError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -180,7 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self,
|
||||
message: str | None = None,
|
||||
media_id: str | None = None,
|
||||
preannounce_media_id: str | None = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Play and show an announcement on the satellite.
|
||||
|
||||
@ -190,7 +190,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_media_id is provided, it 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.
|
||||
"""
|
||||
@ -228,7 +229,7 @@ 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 = None,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Start a conversation from the satellite.
|
||||
|
||||
@ -239,6 +240,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
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.
|
||||
|
||||
Calls async_start_conversation.
|
||||
"""
|
||||
|
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
Binary file not shown.
@ -8,6 +8,7 @@ announce:
|
||||
message:
|
||||
required: false
|
||||
example: "Time to wake up!"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
media_id:
|
||||
@ -28,6 +29,7 @@ start_conversation:
|
||||
start_message:
|
||||
required: false
|
||||
example: "You left the lights on in the living room. Turn them off?"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
start_media_id:
|
||||
|
@ -198,7 +198,8 @@ async def websocket_test_connection(
|
||||
|
||||
hass.async_create_background_task(
|
||||
satellite.async_internal_announce(
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}"
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
|
||||
preannounce_media_id=None,
|
||||
),
|
||||
f"assist_satellite_connection_test_{msg['entity_id']}",
|
||||
)
|
||||
|
@ -4,13 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from hass_nabucasa import Cloud, CloudError
|
||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||
from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError
|
||||
from hass_nabucasa.cloud_api import (
|
||||
FilesHandlerListEntry,
|
||||
async_files_delete_file,
|
||||
@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent):
|
||||
"""
|
||||
if not backup.protected:
|
||||
raise BackupAgentError("Cloud backups must be protected")
|
||||
if self._cloud.subscription_expired:
|
||||
raise BackupAgentError("Cloud subscription has expired")
|
||||
|
||||
size = backup.size
|
||||
try:
|
||||
@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent):
|
||||
) from err
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||
except CloudError as err:
|
||||
if (
|
||||
isinstance(err, CloudApiError)
|
||||
and isinstance(err.orig_exc, ClientResponseError)
|
||||
and err.orig_exc.status == HTTPStatus.FORBIDDEN
|
||||
and self._cloud.subscription_expired
|
||||
):
|
||||
raise BackupAgentError("Cloud subscription has expired") from err
|
||||
if tries == _RETRY_LIMIT:
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||
tries += 1
|
||||
|
@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = {
|
||||
|
||||
|
||||
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
|
||||
DISABLE: 0,
|
||||
HOME_P1: 1,
|
||||
HOME_P2: 2,
|
||||
NIGHT: 3,
|
||||
@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
|
||||
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
|
||||
}.get(self._area.human_status)
|
||||
|
||||
async def _async_update_state(self, area_state: AlarmAreaState, armed: int) -> None:
|
||||
"""Update state after action."""
|
||||
self._area.human_status = area_state
|
||||
self._area.armed = armed
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if code != str(self._api.device_pin):
|
||||
return
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
)
|
||||
|
@ -8,7 +8,7 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@ -98,13 +98,20 @@ class ComelitCoverEntity(
|
||||
"""Return if the cover is opening."""
|
||||
return self._current_action("opening")
|
||||
|
||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||
"""Set desired cover state."""
|
||||
self._last_state = self.state
|
||||
await self._api.set_device_status(COVER, self._device.index, action)
|
||||
self.coordinator.data[COVER][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
await self._api.set_device_status(COVER, self._device.index, STATE_OFF)
|
||||
await self._cover_set_state(STATE_OFF, 2)
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open cover."""
|
||||
await self._api.set_device_status(COVER, self._device.index, STATE_ON)
|
||||
await self._cover_set_state(STATE_ON, 1)
|
||||
|
||||
async def async_stop_cover(self, **_kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
@ -112,13 +119,7 @@ class ComelitCoverEntity(
|
||||
return
|
||||
|
||||
action = STATE_ON if self.is_closing else STATE_OFF
|
||||
await self._api.set_device_status(COVER, self._device.index, action)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle device update."""
|
||||
self._last_state = self.state
|
||||
self.async_write_ha_state()
|
||||
await self._cover_set_state(action, 0)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
@ -59,7 +59,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
|
||||
async def _light_set_state(self, state: int) -> None:
|
||||
"""Set desired light state."""
|
||||
await self.coordinator.api.set_device_status(LIGHT, self._device.index, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[LIGHT][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
|
@ -67,7 +67,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
|
||||
await self.coordinator.api.set_device_status(
|
||||
self._device.type, self._device.index, state
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[self._device.type][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
if (
|
||||
(maybe_result is None) # first result
|
||||
or (num_matched_entities > best_num_matched_entities)
|
||||
or (
|
||||
# More literal text matched
|
||||
result.text_chunks_matched > maybe_result.text_chunks_matched
|
||||
)
|
||||
or (
|
||||
# More entities matched
|
||||
num_matched_entities > best_num_matched_entities
|
||||
)
|
||||
or (
|
||||
# Fewer unmatched entities
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
@ -662,16 +669,6 @@ class DefaultAgent(ConversationEntity):
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
||||
)
|
||||
or (
|
||||
# More literal text matched
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
||||
and (
|
||||
result.text_chunks_matched
|
||||
> maybe_result.text_chunks_matched
|
||||
)
|
||||
)
|
||||
or (
|
||||
# Prefer match failures with entities
|
||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.24"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"]
|
||||
}
|
||||
|
@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
username = auth["cdp_internal_user_id"].lower()
|
||||
username = auth["internalUserID"].lower()
|
||||
await self.async_set_unique_id(username)
|
||||
self._abort_if_unique_id_configured()
|
||||
email = auth["email"].lower()
|
||||
email = auth["loginEmailAddress"].lower()
|
||||
data = {
|
||||
CONF_EMAIL: email,
|
||||
CONF_USERNAME: username,
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodukeenergy==0.2.2"]
|
||||
"requirements": ["aiodukeenergy==0.3.0"]
|
||||
}
|
||||
|
@ -91,15 +91,15 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
|
||||
def operation_list(self) -> list[str]:
|
||||
"""List of available operation modes."""
|
||||
econet_modes = self.water_heater.modes
|
||||
op_list = []
|
||||
operation_modes = set()
|
||||
for mode in econet_modes:
|
||||
if (
|
||||
mode is not WaterHeaterOperationMode.UNKNOWN
|
||||
and mode is not WaterHeaterOperationMode.VACATION
|
||||
):
|
||||
ha_mode = ECONET_STATE_TO_HA[mode]
|
||||
op_list.append(ha_mode)
|
||||
return op_list
|
||||
operation_modes.add(ha_mode)
|
||||
return list(operation_modes)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||
|
@ -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.3.1"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
|
||||
}
|
||||
|
@ -100,7 +100,11 @@ class ElkEntity(Entity):
|
||||
return {"index": self._element.index + 1}
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
pass
|
||||
"""Handle changes to the element.
|
||||
|
||||
This method is called when the element changes. It should be
|
||||
overridden by subclasses to handle the changes.
|
||||
"""
|
||||
|
||||
@callback
|
||||
def _element_callback(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
@ -111,7 +115,7 @@ class ElkEntity(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback for ElkM1 changes and update entity state."""
|
||||
self._element.add_callback(self._element_callback)
|
||||
self._element_callback(self._element, {})
|
||||
self._element_changed(self._element, {})
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
@ -128,8 +128,23 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._password = ""
|
||||
return await self._async_authenticate_or_add()
|
||||
|
||||
if error is None and entry_data.get(CONF_NOISE_PSK):
|
||||
return await self.async_step_reauth_encryption_removed_confirm()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_encryption_removed_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauthorization flow when encryption was removed."""
|
||||
if user_input is not None:
|
||||
self._noise_psk = None
|
||||
return self._async_get_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_encryption_removed_confirm",
|
||||
description_placeholders={"name": self._name},
|
||||
)
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
@ -282,15 +282,18 @@ class RuntimeEntryData:
|
||||
) -> None:
|
||||
"""Distribute an update of static infos to all platforms."""
|
||||
# First, load all platforms
|
||||
needed_platforms = set()
|
||||
if async_get_dashboard(hass):
|
||||
needed_platforms.add(Platform.UPDATE)
|
||||
needed_platforms: set[Platform] = set()
|
||||
|
||||
if self.device_info and self.device_info.voice_assistant_feature_flags_compat(
|
||||
self.api_version
|
||||
):
|
||||
needed_platforms.add(Platform.BINARY_SENSOR)
|
||||
needed_platforms.add(Platform.SELECT)
|
||||
if self.device_info:
|
||||
if async_get_dashboard(hass):
|
||||
# Only load the update platform if the device_info is set
|
||||
# When we restore the entry, the device_info may not be set yet
|
||||
# and we don't want to load the update platform since it needs
|
||||
# a complete device_info.
|
||||
needed_platforms.add(Platform.UPDATE)
|
||||
if self.device_info.voice_assistant_feature_flags_compat(self.api_version):
|
||||
needed_platforms.add(Platform.BINARY_SENSOR)
|
||||
needed_platforms.add(Platform.SELECT)
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
registry_get_entity = ent_reg.async_get_entity_id
|
||||
@ -312,18 +315,19 @@ class RuntimeEntryData:
|
||||
|
||||
# Make a dict of the EntityInfo by type and send
|
||||
# them to the listeners for each specific EntityInfo type
|
||||
infos_by_type: dict[type[EntityInfo], list[EntityInfo]] = {}
|
||||
infos_by_type: defaultdict[type[EntityInfo], list[EntityInfo]] = defaultdict(
|
||||
list
|
||||
)
|
||||
for info in infos:
|
||||
info_type = type(info)
|
||||
if info_type not in infos_by_type:
|
||||
infos_by_type[info_type] = []
|
||||
infos_by_type[info_type].append(info)
|
||||
infos_by_type[type(info)].append(info)
|
||||
|
||||
callbacks_by_type = self.entity_info_callbacks
|
||||
for type_, entity_infos in infos_by_type.items():
|
||||
if callbacks_ := callbacks_by_type.get(type_):
|
||||
for callback_ in callbacks_:
|
||||
callback_(entity_infos)
|
||||
for type_, callbacks in self.entity_info_callbacks.items():
|
||||
# If all entities for a type are removed, we
|
||||
# still need to call the callbacks with an empty list
|
||||
# to make sure the entities are removed.
|
||||
entity_infos = infos_by_type.get(type_, [])
|
||||
for callback_ in callbacks:
|
||||
callback_(entity_infos)
|
||||
|
||||
# Finally update static info subscriptions
|
||||
for callback_ in self.static_info_update_subscriptions:
|
||||
|
@ -33,6 +33,16 @@ class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity):
|
||||
self._trigger_event(self._state.event_type)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
"""Call when device updates or entry data changes."""
|
||||
super()._on_device_update()
|
||||
if self._entry_data.available:
|
||||
# Event entities should go available directly
|
||||
# when the device comes online and not wait
|
||||
# for the next data push.
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
async_setup_entry = partial(
|
||||
platform_async_setup_entry,
|
||||
|
@ -13,6 +13,7 @@ from aioesphomeapi import (
|
||||
APIConnectionError,
|
||||
APIVersion,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EncryptionHelloAPIError,
|
||||
EntityInfo,
|
||||
HomeassistantServiceCall,
|
||||
InvalidAuthAPIError,
|
||||
@ -570,6 +571,7 @@ class ESPHomeManager:
|
||||
if isinstance(
|
||||
err,
|
||||
(
|
||||
EncryptionHelloAPIError,
|
||||
RequiresEncryptionAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
InvalidAuthAPIError,
|
||||
|
@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.7.0",
|
||||
"aioesphomeapi==29.8.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.12.0"
|
||||
],
|
||||
|
@ -43,6 +43,9 @@
|
||||
},
|
||||
"description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration."
|
||||
},
|
||||
"reauth_encryption_removed_confirm": {
|
||||
"description": "The ESPHome device {name} disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections."
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to add the ESPHome node `{name}` to Home Assistant?",
|
||||
"title": "Discovered ESPHome node"
|
||||
|
@ -193,7 +193,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="max_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_max_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@ -201,7 +200,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="max_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_max_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@ -225,6 +223,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="link_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@ -232,6 +231,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="link_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
|
@ -6,6 +6,7 @@ from typing import Any
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
ClimateEntity,
|
||||
@ -38,7 +39,7 @@ from .sensor import value_scheduled_preset
|
||||
HVAC_MODES = [HVACMode.HEAT, HVACMode.OFF]
|
||||
PRESET_HOLIDAY = "holiday"
|
||||
PRESET_SUMMER = "summer"
|
||||
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT]
|
||||
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
|
||||
SUPPORTED_FEATURES = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
@ -194,6 +195,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
return PRESET_HOLIDAY
|
||||
if self.data.summer_active:
|
||||
return PRESET_SUMMER
|
||||
if self.data.target_temperature == ON_API_TEMPERATURE:
|
||||
return PRESET_BOOST
|
||||
if self.data.target_temperature == self.data.comfort_temperature:
|
||||
return PRESET_COMFORT
|
||||
if self.data.target_temperature == self.data.eco_temperature:
|
||||
@ -211,6 +214,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
await self.async_set_temperature(temperature=self.data.comfort_temperature)
|
||||
elif preset_mode == PRESET_ECO:
|
||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
||||
elif preset_mode == PRESET_BOOST:
|
||||
await self.async_set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250326.0"]
|
||||
"requirements": ["home-assistant-frontend==20250328.0"]
|
||||
}
|
||||
|
@ -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.1"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"]
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google.genai import Client
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from requests.exceptions import Timeout
|
||||
import voluptuous as vol
|
||||
@ -43,7 +43,7 @@ CONF_FILENAMES = "filenames"
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = (Platform.CONVERSATION,)
|
||||
|
||||
type GoogleGenerativeAIConfigEntry = ConfigEntry[genai.Client]
|
||||
type GoogleGenerativeAIConfigEntry = ConfigEntry[Client]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@ -139,7 +139,11 @@ async def async_setup_entry(
|
||||
"""Set up Google Generative AI Conversation from a config entry."""
|
||||
|
||||
try:
|
||||
client = genai.Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
def _init_client() -> Client:
|
||||
return Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
client = await hass.async_add_executor_job(_init_client)
|
||||
await client.aio.models.get(
|
||||
model=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
|
@ -356,6 +356,15 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
messages.append(_convert_content(chat_content))
|
||||
|
||||
# The SDK requires the first message to be a user message
|
||||
# This is not the case if user used `start_conversation`
|
||||
# Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537
|
||||
if messages and messages[0].role != "user":
|
||||
messages.insert(
|
||||
0,
|
||||
Content(role="user", parts=[Part.from_text(text=" ")]),
|
||||
)
|
||||
|
||||
if tool_results:
|
||||
messages.append(_create_google_tool_response_content(tool_results))
|
||||
generateContentConfig = GenerateContentConfig(
|
||||
|
@ -16,13 +16,13 @@
|
||||
"name": "Panel light"
|
||||
},
|
||||
"quiet": {
|
||||
"name": "Quiet"
|
||||
"name": "Quiet mode"
|
||||
},
|
||||
"fresh_air": {
|
||||
"name": "Fresh air"
|
||||
},
|
||||
"xfan": {
|
||||
"name": "XFan"
|
||||
"name": "Xtra fan"
|
||||
},
|
||||
"health_mode": {
|
||||
"name": "Health mode"
|
||||
|
@ -8,7 +8,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "Country"
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
@ -244,6 +244,7 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
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"
|
||||
@ -283,7 +284,8 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
DOMAIN,
|
||||
f"deprecated_binary_common_door_sensor_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.5.0",
|
||||
is_fixable=False,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_binary_common_door_sensor",
|
||||
translation_placeholders={
|
||||
|
@ -64,7 +64,6 @@ set_program_and_options:
|
||||
- selected_program
|
||||
program:
|
||||
example: dishcare_dishwasher_program_auto2
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
|
@ -134,15 +134,47 @@
|
||||
},
|
||||
"deprecated_binary_common_door_sensor": {
|
||||
"title": "Deprecated binary door sensor detected in some automations or scripts",
|
||||
"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."
|
||||
"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",
|
||||
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_command_actions::title%]",
|
||||
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_program_switch_in_automations_scripts": {
|
||||
"title": "Deprecated program switch detected in some automations or scripts",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_program_switch_in_automations_scripts::title%]",
|
||||
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_program_switch": {
|
||||
"title": "Deprecated program switch detected in some automations or scripts",
|
||||
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
|
||||
"title": "Deprecated program switch entities",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_program_switch::title%]",
|
||||
"description": "The switch entity `{entity_id}` and all the other program switches are deprecated.\n\nPlease use the active program select entity instead."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_set_program_and_option_actions": {
|
||||
"title": "The executed action is deprecated",
|
||||
|
@ -266,7 +266,10 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
SwitchEntityDescription(key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM),
|
||||
SwitchEntityDescription(
|
||||
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
self._attr_name = f"{appliance.info.name} {desc}"
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-{desc}"
|
||||
@ -304,11 +307,12 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_{self.entity_id}",
|
||||
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.6.0",
|
||||
is_fixable=False,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_program_switch",
|
||||
translation_key="deprecated_program_switch_in_automations_scripts",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"items": "\n".join(items_list),
|
||||
@ -317,12 +321,34 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed from hass."""
|
||||
async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
|
||||
)
|
||||
async_delete_issue(
|
||||
self.hass, DOMAIN, f"deprecated_program_switch_{self.entity_id}"
|
||||
)
|
||||
|
||||
def create_action_handler_issue(self) -> None:
|
||||
"""Create deprecation issue."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.6.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_program_switch",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Start the program."""
|
||||
self.create_action_handler_issue()
|
||||
try:
|
||||
await self.coordinator.client.start_program(
|
||||
self.appliance.info.ha_id, program_key=self.program.key
|
||||
@ -339,6 +365,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Stop the program."""
|
||||
self.create_action_handler_issue()
|
||||
try:
|
||||
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
|
||||
except HomeConnectError as err:
|
||||
|
@ -31,7 +31,6 @@ class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
|
||||
_LOGGER,
|
||||
name="firmware update coordinator",
|
||||
update_interval=FIRMWARE_REFRESH_INTERVAL,
|
||||
always_update=False,
|
||||
)
|
||||
self.hass = hass
|
||||
self.session = session
|
||||
|
@ -199,7 +199,7 @@ class BaseFirmwareUpdateEntity(
|
||||
# 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"
|
||||
self._attr_title = self.entity_description.firmware_name or "Unknown"
|
||||
|
||||
if (
|
||||
self._current_firmware_info is None
|
||||
|
@ -15,14 +15,13 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant SkyConnect config entry."""
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
|
@ -21,11 +21,20 @@ from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
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 DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import FIRMWARE, FIRMWARE_VERSION, NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
HardwareVariant,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -42,7 +51,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
fw_type="skyconnect_zigbee_ncp",
|
||||
version_key="ezsp_version",
|
||||
expected_firmware_type=ApplicationType.EZSP,
|
||||
firmware_name="EmberZNet",
|
||||
firmware_name="EmberZNet Zigbee",
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
@ -55,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
expected_firmware_type=ApplicationType.SPINEL,
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type="skyconnect_multipan",
|
||||
version_key="cpc_version",
|
||||
expected_firmware_type=ApplicationType.CPC,
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None, # We don't want to update the bootloader
|
||||
version_key="gecko_bootloader_version",
|
||||
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
|
||||
firmware_name="Gecko Bootloader",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
@ -77,9 +108,16 @@ def _async_create_update_entity(
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
firmware_type = config_entry.data[FIRMWARE]
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type) if firmware_type is not None else None
|
||||
]
|
||||
|
||||
try:
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type)
|
||||
]
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.debug(
|
||||
"Unknown firmware type %r, using default entity description", firmware_type
|
||||
)
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
|
||||
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=config_entry.data["device"],
|
||||
@ -130,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""SkyConnect firmware update entity."""
|
||||
|
||||
bootloader_reset_type = None
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -141,8 +180,18 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Initialize the SkyConnect firmware update entity."""
|
||||
super().__init__(device, config_entry, update_coordinator, entity_description)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._config_entry.data['serial_number']}_{self.entity_description.key}"
|
||||
variant = HardwareVariant.from_usb_product_name(
|
||||
self._config_entry.data[PRODUCT]
|
||||
)
|
||||
serial_number = self._config_entry.data[SERIAL_NUMBER]
|
||||
|
||||
self._attr_unique_id = f"{serial_number}_{self.entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=f"{variant.full_name} ({serial_number[:8]})",
|
||||
model=variant.full_name,
|
||||
manufacturer="Nabu Casa",
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
# Use the cached firmware info if it exists
|
||||
@ -155,6 +204,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
source="homeassistant_sky_connect",
|
||||
)
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
super()._update_attributes()
|
||||
|
||||
assert self.device_entry is not None
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_update_device(
|
||||
device_id=self.device_entry.id,
|
||||
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
|
||||
"""Handle updated firmware info being pushed by an integration."""
|
||||
|
@ -62,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
DOMAIN = "homeassistant_yellow"
|
||||
|
||||
RADIO_MODEL = "Home Assistant Yellow"
|
||||
RADIO_MANUFACTURER = "Nabu Casa"
|
||||
MODEL = "Home Assistant Yellow"
|
||||
MANUFACTURER = "Nabu Casa"
|
||||
|
||||
RADIO_DEVICE = "/dev/ttyAMA1"
|
||||
|
||||
ZHA_HW_DISCOVERY_DATA = {
|
||||
|
@ -149,5 +149,12 @@
|
||||
"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%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"firmware": {
|
||||
"name": "Radio firmware"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,13 +21,17 @@ from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
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 DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
MODEL,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
RADIO_DEVICE,
|
||||
)
|
||||
@ -39,7 +43,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
ApplicationType | None, FirmwareUpdateEntityDescription
|
||||
] = {
|
||||
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -47,10 +51,10 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
fw_type="yellow_zigbee_ncp",
|
||||
version_key="ezsp_version",
|
||||
expected_firmware_type=ApplicationType.EZSP,
|
||||
firmware_name="EmberZNet",
|
||||
firmware_name="EmberZNet Zigbee",
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -60,12 +64,34 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
expected_firmware_type=ApplicationType.SPINEL,
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type="yellow_multipan",
|
||||
version_key="cpc_version",
|
||||
expected_firmware_type=ApplicationType.CPC,
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None, # We don't want to update the bootloader
|
||||
version_key="gecko_bootloader_version",
|
||||
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
|
||||
firmware_name="Gecko Bootloader",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None,
|
||||
version_key=None,
|
||||
expected_firmware_type=None,
|
||||
@ -82,9 +108,16 @@ def _async_create_update_entity(
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
firmware_type = config_entry.data[FIRMWARE]
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type) if firmware_type is not None else None
|
||||
]
|
||||
|
||||
try:
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type)
|
||||
]
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.debug(
|
||||
"Unknown firmware type %r, using default entity description", firmware_type
|
||||
)
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
|
||||
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=RADIO_DEVICE,
|
||||
@ -135,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Yellow firmware update entity."""
|
||||
|
||||
bootloader_reset_type = "yellow" # Triggers a GPIO reset
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -145,8 +179,13 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
) -> None:
|
||||
"""Initialize the Yellow firmware update entity."""
|
||||
super().__init__(device, config_entry, update_coordinator, entity_description)
|
||||
|
||||
self._attr_unique_id = self.entity_description.key
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, "yellow")},
|
||||
name=MODEL,
|
||||
model=MODEL,
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
|
||||
# Use the cached firmware info if it exists
|
||||
if self._config_entry.data[FIRMWARE] is not None:
|
||||
@ -158,6 +197,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
source="homeassistant_yellow",
|
||||
)
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
super()._update_attributes()
|
||||
|
||||
assert self.device_entry is not None
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_update_device(
|
||||
device_id=self.device_entry.id,
|
||||
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
|
||||
"""Handle updated firmware info being pushed by an integration."""
|
||||
|
@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.8"],
|
||||
"requirements": ["aiohomekit==3.2.13"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
@ -1,4 +1,28 @@
|
||||
{
|
||||
"entity": {
|
||||
"light": {
|
||||
"hue_light": {
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
"state": {
|
||||
"candle": "mdi:candle",
|
||||
"sparkle": "mdi:shimmer",
|
||||
"glisten": "mdi:creation",
|
||||
"sunrise": "mdi:weather-sunset-up",
|
||||
"sunset": "mdi:weather-sunset",
|
||||
"fire": "mdi:fire",
|
||||
"prism": "mdi:triangle-outline",
|
||||
"opal": "mdi:diamond-stone",
|
||||
"underwater": "mdi:waves",
|
||||
"cosmos": "mdi:star-shooting",
|
||||
"sunbeam": "mdi:spotlight-beam",
|
||||
"enchant": "mdi:magic-staff"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"hue_activate_scene": {
|
||||
"service": "mdi:palette"
|
||||
|
@ -227,12 +227,16 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
||||
@callback
|
||||
def _get_current_work_area_name(data: MowerAttributes) -> str:
|
||||
"""Return the name of the current work area."""
|
||||
if data.mower.work_area_id is None:
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
if TYPE_CHECKING:
|
||||
# Sensor does not get created if values are None
|
||||
assert data.work_areas is not None
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
if (
|
||||
data.mower.work_area_id is not None
|
||||
and data.mower.work_area_id in data.work_areas
|
||||
):
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["iaqualink"],
|
||||
"requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
|
||||
"requirements": ["iaqualink==0.5.3", "h2==4.1.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ async def async_setup_entry(
|
||||
for vtype, _, vid in isy.variables.children:
|
||||
numbers.append(isy.variables[vtype][vid])
|
||||
if (
|
||||
isy.conf[CONFIG_NETWORKING] or isy.conf[CONFIG_PORTAL]
|
||||
isy.conf[CONFIG_NETWORKING] or isy.conf.get(CONFIG_PORTAL)
|
||||
) and isy.networking.nobjs:
|
||||
isy_data.devices[CONF_NETWORK] = _create_service_device_info(
|
||||
isy, name=CONFIG_NETWORKING, unique_id=CONF_NETWORK
|
||||
|
@ -24,7 +24,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyisy"],
|
||||
"requirements": ["pyisy==3.1.14"],
|
||||
"requirements": ["pyisy==3.1.15"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Universal Devices Inc.",
|
||||
|
@ -6,6 +6,7 @@ count_omer:
|
||||
selector:
|
||||
date:
|
||||
nusach:
|
||||
required: true
|
||||
example: "sfarad"
|
||||
default: "sfarad"
|
||||
selector:
|
||||
|
@ -2,19 +2,19 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"import_confirm": {
|
||||
"title": "Import Konnected Device",
|
||||
"description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
|
||||
"title": "Import Konnected device",
|
||||
"description": "A Konnected alarm panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
|
||||
},
|
||||
"user": {
|
||||
"description": "Please enter the host information for your Konnected Panel.",
|
||||
"description": "Please enter the host information for your Konnected panel.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::ip%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Konnected Device Ready",
|
||||
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings."
|
||||
"title": "Konnected device ready",
|
||||
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected alarm panel settings."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@ -45,8 +45,8 @@
|
||||
}
|
||||
},
|
||||
"options_io_ext": {
|
||||
"title": "Configure Extended I/O",
|
||||
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
|
||||
"title": "Configure extended I/O",
|
||||
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
|
||||
"data": {
|
||||
"8": "Zone 8",
|
||||
"9": "Zone 9",
|
||||
@ -59,25 +59,25 @@
|
||||
}
|
||||
},
|
||||
"options_binary": {
|
||||
"title": "Configure Binary Sensor",
|
||||
"title": "Configure binary sensor",
|
||||
"description": "{zone} options",
|
||||
"data": {
|
||||
"type": "Binary Sensor Type",
|
||||
"type": "Binary sensor type",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"inverse": "Invert the open/close state"
|
||||
}
|
||||
},
|
||||
"options_digital": {
|
||||
"title": "Configure Digital Sensor",
|
||||
"title": "Configure digital sensor",
|
||||
"description": "[%key:component::konnected::options::step::options_binary::description%]",
|
||||
"data": {
|
||||
"type": "Sensor Type",
|
||||
"type": "Sensor type",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"poll_interval": "Poll Interval (minutes)"
|
||||
"poll_interval": "Poll interval (minutes)"
|
||||
}
|
||||
},
|
||||
"options_switch": {
|
||||
"title": "Configure Switchable Output",
|
||||
"title": "Configure switchable output",
|
||||
"description": "{zone} options: state {state}",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
@ -89,18 +89,18 @@
|
||||
}
|
||||
},
|
||||
"options_misc": {
|
||||
"title": "Configure Misc",
|
||||
"title": "Configure misc",
|
||||
"description": "Please select the desired behavior for your panel",
|
||||
"data": {
|
||||
"discovery": "Respond to discovery requests on your network",
|
||||
"blink": "Blink panel LED on when sending state change",
|
||||
"override_api_host": "Override default Home Assistant API host panel URL",
|
||||
"api_host": "Override API host URL"
|
||||
"override_api_host": "Override default Home Assistant API host URL",
|
||||
"api_host": "Custom API host URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"bad_host": "Invalid Override API host URL"
|
||||
"bad_host": "Invalid custom API host URL"
|
||||
},
|
||||
"abort": {
|
||||
"not_konn_panel": "[%key:component::konnected::config::abort::not_konn_panel%]"
|
||||
|
@ -1,7 +1,15 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:lightbulb"
|
||||
"default": "mdi:lightbulb",
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
"default": "mdi:circle-medium",
|
||||
"state": {
|
||||
"off": "mdi:star-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -93,7 +93,10 @@
|
||||
"name": "Color temperature (Kelvin)"
|
||||
},
|
||||
"effect": {
|
||||
"name": "Effect"
|
||||
"name": "Effect",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"effect_list": {
|
||||
"name": "Available effects"
|
||||
|
@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.1"],
|
||||
"requirements": ["python-linkplay==0.2.2"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.0.1"]
|
||||
"requirements": ["ical==9.0.3"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.0.1"]
|
||||
"requirements": ["ical==9.0.3"]
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ class MatterEventEntity(MatterEntity, EventEntity):
|
||||
max_presses_supported = self.get_matter_attribute_value(
|
||||
clusters.Switch.Attributes.MultiPressMax
|
||||
)
|
||||
max_presses_supported = min(max_presses_supported or 1, 8)
|
||||
max_presses_supported = min(max_presses_supported or 2, 8)
|
||||
for i in range(max_presses_supported):
|
||||
event_types.append(f"multi_press_{i + 1}") # noqa: PERF401
|
||||
elif feature_map & SwitchFeature.kMomentarySwitch:
|
||||
|
@ -23,7 +23,11 @@ from homeassistant.helpers.network import (
|
||||
from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType
|
||||
|
||||
# Paths that we don't need to sign
|
||||
PATHS_WITHOUT_AUTH = ("/api/tts_proxy/", "/api/esphome/ffmpeg_proxy/")
|
||||
PATHS_WITHOUT_AUTH = (
|
||||
"/api/tts_proxy/",
|
||||
"/api/esphome/ffmpeg_proxy/",
|
||||
"/api/assist_satellite/static/",
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -6,7 +6,7 @@
|
||||
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]",
|
||||
"medium_type": "Medium Type"
|
||||
"medium_type": "Medium type"
|
||||
}
|
||||
},
|
||||
"bluetooth_confirm": {
|
||||
|
@ -153,7 +153,6 @@ from .util import (
|
||||
learn_more_url,
|
||||
valid_birth_will,
|
||||
valid_publish_topic,
|
||||
valid_qos_schema,
|
||||
valid_subscribe_topic,
|
||||
valid_subscribe_topic_template,
|
||||
)
|
||||
@ -182,7 +181,6 @@ PASSWORD_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWO
|
||||
QOS_SELECTOR = NumberSelector(
|
||||
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2)
|
||||
)
|
||||
QOS_DATA_SCHEMA = vol.All(QOS_SELECTOR, valid_qos_schema)
|
||||
KEEPALIVE_SELECTOR = vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
@ -1145,7 +1143,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
|
||||
"birth_payload", description={"suggested_value": birth[CONF_PAYLOAD]}
|
||||
)
|
||||
] = TEXT_SELECTOR
|
||||
fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_DATA_SCHEMA
|
||||
fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_SELECTOR
|
||||
fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = (
|
||||
BOOLEAN_SELECTOR
|
||||
)
|
||||
@ -1168,7 +1166,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
|
||||
"will_payload", description={"suggested_value": will[CONF_PAYLOAD]}
|
||||
)
|
||||
] = TEXT_SELECTOR
|
||||
fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_DATA_SCHEMA
|
||||
fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_SELECTOR
|
||||
fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = (
|
||||
BOOLEAN_SELECTOR
|
||||
)
|
||||
@ -1269,13 +1267,9 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
reconfig=True,
|
||||
)
|
||||
if user_input is not None:
|
||||
merged_user_input, errors = validate_user_input(
|
||||
user_input, MQTT_DEVICE_PLATFORM_FIELDS
|
||||
)
|
||||
_, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
|
||||
if not errors:
|
||||
self._subentry_data[CONF_DEVICE] = cast(
|
||||
MqttDeviceData, merged_user_input
|
||||
)
|
||||
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, user_input)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
return await self.async_step_summary_menu()
|
||||
return await self.async_step_entity()
|
||||
|
@ -126,7 +126,7 @@
|
||||
"payload_not_available": "Payload not available"
|
||||
},
|
||||
"data_description": {
|
||||
"availability_topic": "Topic to receive the availabillity payload on",
|
||||
"availability_topic": "Topic to receive the availability payload on",
|
||||
"availability_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-templates-with-the-mqtt-integration) to render the availability payload received on the availability topic",
|
||||
"payload_available": "The payload that indicates the device is available (defaults to 'online')",
|
||||
"payload_not_available": "The payload that indicates the device is not available (defaults to 'offline')"
|
||||
@ -219,10 +219,10 @@
|
||||
"options": "Add option"
|
||||
},
|
||||
"data_description": {
|
||||
"device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)",
|
||||
"state_class": "The [state_class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)",
|
||||
"device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)",
|
||||
"state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)",
|
||||
"unit_of_measurement": "Defines the unit of measurement of the sensor, if any.",
|
||||
"options": "Options for allowed sensor state values. The sensor’s device_class must be set to Enumeration. The options option cannot be used together with State Class or Unit of measurement."
|
||||
"options": "Options for allowed sensor state values. The sensor’s Device class must be set to Enumeration. The 'Options' setting cannot be used together with State class or Unit of measurement."
|
||||
},
|
||||
"sections": {
|
||||
"advanced_settings": {
|
||||
@ -285,9 +285,9 @@
|
||||
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
|
||||
"invalid_url": "Invalid URL",
|
||||
"options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used",
|
||||
"options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class'. If you continue, the existing options will be reset",
|
||||
"options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class. If you continue, the existing options will be reset",
|
||||
"options_with_enum_device_class": "Configure options for the enumeration sensor",
|
||||
"uom_required_for_device_class": "The selected device device class requires a unit"
|
||||
"uom_required_for_device_class": "The selected device class requires a unit"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -453,7 +453,7 @@
|
||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
|
||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
||||
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
||||
|
@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["music_assistant"],
|
||||
"requirements": ["music-assistant-client==1.1.1"],
|
||||
"requirements": ["music-assistant-client==1.2.0"],
|
||||
"zeroconf": ["_mass._tcp.local."]
|
||||
}
|
||||
|
@ -94,6 +94,12 @@ SUPPORTED_FEATURES_BASE = (
|
||||
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
||||
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
# we always add pause support,
|
||||
# regardless if the underlying player actually natively supports pause
|
||||
# because the MA behavior is to internally handle pause with stop
|
||||
# (and a resume position) and we'd like to keep the UX consistent
|
||||
# background info: https://github.com/home-assistant/core/issues/140118
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
)
|
||||
|
||||
QUEUE_OPTION_MAP = {
|
||||
@ -697,8 +703,6 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
supported_features = SUPPORTED_FEATURES_BASE
|
||||
if PlayerFeature.SET_MEMBERS in self.player.supported_features:
|
||||
supported_features |= MediaPlayerEntityFeature.GROUPING
|
||||
if PlayerFeature.PAUSE in self.player.supported_features:
|
||||
supported_features |= MediaPlayerEntityFeature.PAUSE
|
||||
if self.player.mute_control != PLAYER_CONTROL_NONE:
|
||||
supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
if self.player.volume_control != PLAYER_CONTROL_NONE:
|
||||
|
@ -360,7 +360,7 @@ class NMBSSensor(SensorEntity):
|
||||
attrs[ATTR_LONGITUDE] = self.station_coordinates[1]
|
||||
|
||||
if self.is_via_connection and not self._excl_vias:
|
||||
via = self._attrs.vias.via[0]
|
||||
via = self._attrs.vias[0]
|
||||
|
||||
attrs["via"] = via.station
|
||||
attrs["via_arrival_platform"] = via.arrival.platform
|
||||
|
@ -104,6 +104,15 @@ def _resize_image(image, opts):
|
||||
new_width = opts.max_width
|
||||
(old_width, old_height) = img.size
|
||||
old_size = len(image)
|
||||
|
||||
# If no max_width specified, only apply quality changes if requested
|
||||
if new_width is None:
|
||||
if opts.quality is None:
|
||||
return image
|
||||
imgbuf = io.BytesIO()
|
||||
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
|
||||
return imgbuf.getvalue()
|
||||
|
||||
if old_width <= new_width:
|
||||
if opts.quality is None:
|
||||
_LOGGER.debug("Image is smaller-than/equal-to requested width")
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pvoutput",
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pvo==2.2.0"]
|
||||
"requirements": ["pvo==2.2.1"]
|
||||
}
|
||||
|
@ -27,19 +27,19 @@
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"energy_consumption": {
|
||||
"name": "Energy consumed"
|
||||
"name": "Energy consumption"
|
||||
},
|
||||
"energy_generation": {
|
||||
"name": "Energy generated"
|
||||
"name": "Energy generation"
|
||||
},
|
||||
"efficiency": {
|
||||
"name": "Efficiency"
|
||||
},
|
||||
"power_consumption": {
|
||||
"name": "Power consumed"
|
||||
"name": "Power consumption"
|
||||
},
|
||||
"power_generation": {
|
||||
"name": "Power generated"
|
||||
"name": "Power generation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ class PyLoadData:
|
||||
download: bool
|
||||
reconnect: bool
|
||||
captcha: bool | None = None
|
||||
proxy: bool | None = None
|
||||
free_space: int
|
||||
|
||||
|
||||
|
@ -42,6 +42,10 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._async_abort_entries_match(
|
||||
{CONF_CALENDAR_NAME: user_input[CONF_CALENDAR_NAME]}
|
||||
)
|
||||
if user_input[CONF_URL].startswith("webcal://"):
|
||||
user_input[CONF_URL] = user_input[CONF_URL].replace(
|
||||
"webcal://", "https://", 1
|
||||
)
|
||||
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
|
||||
client = get_async_client(self.hass)
|
||||
try:
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==9.0.1"]
|
||||
"requirements": ["ical==9.0.3"]
|
||||
}
|
||||
|
@ -143,6 +143,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow started by a dhcp discovery."""
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device = device_registry.async_get_device(
|
||||
connections={
|
||||
|
@ -278,10 +278,10 @@
|
||||
"name": "Timestamp"
|
||||
},
|
||||
"volatile_organic_compounds": {
|
||||
"name": "VOCs"
|
||||
"name": "Volatile organic compounds"
|
||||
},
|
||||
"volatile_organic_compounds_parts": {
|
||||
"name": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]"
|
||||
"name": "Volatile organic compounds parts"
|
||||
},
|
||||
"voltage": {
|
||||
"name": "Voltage"
|
||||
|
@ -352,7 +352,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return {
|
||||
"new_unique_id": f"{device_id}_{MAIN}_{Capability.THREE_AXIS}_{Attribute.THREE_AXIS}_{new_attribute}",
|
||||
}
|
||||
if attribute == Attribute.MACHINE_STATE:
|
||||
if attribute in {
|
||||
Attribute.MACHINE_STATE,
|
||||
Attribute.COMPLETION_TIME,
|
||||
}:
|
||||
capability = determine_machine_type(
|
||||
hass, entry.entry_id, device_id
|
||||
)
|
||||
@ -410,7 +413,9 @@ def create_devices(
|
||||
rooms: dict[str, str],
|
||||
) -> None:
|
||||
"""Create devices in the device registry."""
|
||||
for device in devices.values():
|
||||
for device in sorted(
|
||||
devices.values(), key=lambda d: d.device.parent_device_id or ""
|
||||
):
|
||||
kwargs: dict[str, Any] = {}
|
||||
if device.device.hub is not None:
|
||||
kwargs = {
|
||||
@ -421,7 +426,7 @@ def create_devices(
|
||||
kwargs[ATTR_CONNECTIONS] = {
|
||||
(dr.CONNECTION_NETWORK_MAC, device.device.hub.mac_address)
|
||||
}
|
||||
if device.device.parent_device_id:
|
||||
if device.device.parent_device_id and device.device.parent_device_id in devices:
|
||||
kwargs[ATTR_VIA_DEVICE] = (DOMAIN, device.device.parent_device_id)
|
||||
if (ocf := device.device.ocf) is not None:
|
||||
kwargs.update(
|
||||
|
@ -58,5 +58,6 @@ class SmartThingsButtonEvent(SmartThingsEntity, EventEntity):
|
||||
)
|
||||
|
||||
def _update_handler(self, event: DeviceEvent) -> None:
|
||||
self._trigger_event(cast(str, event.value))
|
||||
self.async_write_ha_state()
|
||||
if event.attribute is Attribute.BUTTON:
|
||||
self._trigger_event(cast(str, event.value))
|
||||
super()._update_handler(event)
|
||||
|
@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.0.0"]
|
||||
"requirements": ["pysmartthings==3.0.1"]
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from pysmartthings import Attribute, Capability, Command, SmartThings
|
||||
|
||||
from homeassistant.components.number import NumberEntity
|
||||
from homeassistant.components.number import NumberEntity, NumberMode
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@ -32,6 +32,7 @@ class SmartThingsWasherRinseCyclesNumberEntity(SmartThingsEntity, NumberEntity):
|
||||
|
||||
_attr_translation_key = "washer_rinse_cycles"
|
||||
_attr_native_step = 1.0
|
||||
_attr_mode = NumberMode.BOX
|
||||
|
||||
def __init__(self, client: SmartThings, device: FullDevice) -> None:
|
||||
"""Initialize the instance."""
|
||||
|
@ -331,7 +331,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
translation_key="dryer_machine_state",
|
||||
options=WASHER_OPTIONS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
deprecated=lambda _: "machine_state",
|
||||
)
|
||||
],
|
||||
Attribute.DRYER_JOB_STATE: [
|
||||
@ -966,7 +965,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
translation_key="washer_machine_state",
|
||||
options=WASHER_OPTIONS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
deprecated=lambda _: "machine_state",
|
||||
)
|
||||
],
|
||||
Attribute.WASHER_JOB_STATE: [
|
||||
|
@ -487,10 +487,6 @@
|
||||
"title": "Deprecated refrigerator door binary sensor detected in some automations or scripts",
|
||||
"description": "The refrigerator door binary sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts to fix this issue."
|
||||
},
|
||||
"deprecated_machine_state": {
|
||||
"title": "Deprecated machine state sensor detected in some automations or scripts",
|
||||
"description": "The machine state sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nA select entity is now available for the machine state and should be used going forward. Please use the new select entity in the above automations or scripts to fix this issue."
|
||||
},
|
||||
"deprecated_switch_appliance": {
|
||||
"title": "Deprecated switch detected in some automations or scripts",
|
||||
"description": "The switch `{entity}` is deprecated because the actions did not work, so it has been replaced with a binary sensor instead.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new binary sensor in the above automations or scripts to fix this issue."
|
||||
|
@ -31,6 +31,7 @@ async def async_setup_entry(
|
||||
"power",
|
||||
"status_requested",
|
||||
"sticky_white_noise_updated",
|
||||
"config_change",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["snoo"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-snoo==0.6.4"]
|
||||
"requirements": ["python-snoo==0.6.5"]
|
||||
}
|
||||
|
@ -55,7 +55,8 @@
|
||||
"activity": "Activity press",
|
||||
"power": "Power button pressed",
|
||||
"status_requested": "Status requested",
|
||||
"sticky_white_noise_updated": "Sleepytime sounds updated"
|
||||
"sticky_white_noise_updated": "Sleepytime sounds updated",
|
||||
"config_change": "Config changed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"title": "Define the API parameters for this installation",
|
||||
"data": {
|
||||
"name": "The name of this installation",
|
||||
"site_id": "The SolarEdge site-id",
|
||||
"site_id": "The SolarEdge site ID",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"site_not_active": "The site is not active",
|
||||
"could_not_connect": "Could not connect to the solaredge API"
|
||||
"could_not_connect": "Could not connect to the SolarEdge API"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
@ -65,7 +65,7 @@
|
||||
"name": "Grid power"
|
||||
},
|
||||
"storage_power": {
|
||||
"name": "Stored power"
|
||||
"name": "Storage power"
|
||||
},
|
||||
"purchased_energy": {
|
||||
"name": "Imported energy"
|
||||
|
@ -1,197 +1,39 @@
|
||||
"""The Sun WEG inverter sensor integration."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
|
||||
from sunweg.api import APIHelper
|
||||
from sunweg.plant import Plant
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.typing import StateType, UndefinedType
|
||||
from homeassistant.util import Throttle
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .const import CONF_PLANT_ID, DOMAIN, PLATFORMS, DeviceType
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(minutes=5)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DOMAIN = "sunweg"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
"""Load the saved entities."""
|
||||
api = APIHelper(entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD])
|
||||
if not await hass.async_add_executor_job(api.authenticate):
|
||||
raise ConfigEntryAuthFailed("Username or Password may be incorrect!")
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SunWEGData(
|
||||
api, entry.data[CONF_PLANT_ID]
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"issue": "https://github.com/rokam/sunweg/issues/13",
|
||||
"entries": "/config/integrations/integration/sunweg",
|
||||
},
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
if len(hass.data[DOMAIN]) == 0:
|
||||
hass.data.pop(DOMAIN)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
class SunWEGData:
|
||||
"""The class for handling data retrieval."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api: APIHelper,
|
||||
plant_id: int,
|
||||
) -> None:
|
||||
"""Initialize the probe."""
|
||||
|
||||
self.api = api
|
||||
self.plant_id = plant_id
|
||||
self.data: Plant = None
|
||||
self.previous_values: dict = {}
|
||||
|
||||
@Throttle(SCAN_INTERVAL)
|
||||
def update(self) -> None:
|
||||
"""Update probe data."""
|
||||
_LOGGER.debug("Updating data for plant %s", self.plant_id)
|
||||
try:
|
||||
self.data = self.api.plant(self.plant_id)
|
||||
for inverter in self.data.inverters:
|
||||
self.api.complete_inverter(inverter)
|
||||
except json.decoder.JSONDecodeError:
|
||||
_LOGGER.error("Unable to fetch data from SunWEG server")
|
||||
_LOGGER.debug("Finished updating data for plant %s", self.plant_id)
|
||||
|
||||
def get_api_value(
|
||||
self,
|
||||
variable: str,
|
||||
device_type: DeviceType,
|
||||
inverter_id: int = 0,
|
||||
deep_name: str | None = None,
|
||||
):
|
||||
"""Retrieve from a Plant the desired variable value."""
|
||||
if device_type == DeviceType.TOTAL:
|
||||
return self.data.__dict__.get(variable)
|
||||
|
||||
inverter_list = [i for i in self.data.inverters if i.id == inverter_id]
|
||||
if len(inverter_list) == 0:
|
||||
return None
|
||||
inverter = inverter_list[0]
|
||||
|
||||
if device_type == DeviceType.INVERTER:
|
||||
return inverter.__dict__.get(variable)
|
||||
if device_type == DeviceType.PHASE:
|
||||
for phase in inverter.phases:
|
||||
if phase.name == deep_name:
|
||||
return phase.__dict__.get(variable)
|
||||
elif device_type == DeviceType.STRING:
|
||||
for mppt in inverter.mppts:
|
||||
for string in mppt.strings:
|
||||
if string.name == deep_name:
|
||||
return string.__dict__.get(variable)
|
||||
return None
|
||||
|
||||
def get_data(
|
||||
self,
|
||||
*,
|
||||
api_variable_key: str,
|
||||
api_variable_unit: str | None,
|
||||
deep_name: str | None,
|
||||
device_type: DeviceType,
|
||||
inverter_id: int,
|
||||
name: str | UndefinedType | None,
|
||||
native_unit_of_measurement: str | None,
|
||||
never_resets: bool,
|
||||
previous_value_drop_threshold: float | None,
|
||||
) -> tuple[StateType | datetime.datetime, str | None]:
|
||||
"""Get the data."""
|
||||
_LOGGER.debug(
|
||||
"Data request for: %s",
|
||||
name,
|
||||
)
|
||||
variable = api_variable_key
|
||||
previous_unit = native_unit_of_measurement
|
||||
api_value = self.get_api_value(variable, device_type, inverter_id, deep_name)
|
||||
previous_value = self.previous_values.get(variable)
|
||||
return_value = api_value
|
||||
if api_variable_unit is not None:
|
||||
native_unit_of_measurement = self.get_api_value(
|
||||
api_variable_unit,
|
||||
device_type,
|
||||
inverter_id,
|
||||
deep_name,
|
||||
)
|
||||
|
||||
# If we have a 'drop threshold' specified, then check it and correct if needed
|
||||
if (
|
||||
previous_value_drop_threshold is not None
|
||||
and previous_value is not None
|
||||
and api_value is not None
|
||||
and previous_unit == native_unit_of_measurement
|
||||
):
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"%s - Drop threshold specified (%s), checking for drop... API"
|
||||
" Value: %s, Previous Value: %s"
|
||||
),
|
||||
name,
|
||||
previous_value_drop_threshold,
|
||||
api_value,
|
||||
previous_value,
|
||||
)
|
||||
diff = float(api_value) - float(previous_value)
|
||||
|
||||
# Check if the value has dropped (negative value i.e. < 0) and it has only
|
||||
# dropped by a small amount, if so, use the previous value.
|
||||
# Note - The energy dashboard takes care of drops within 10%
|
||||
# of the current value, however if the value is low e.g. 0.2
|
||||
# and drops by 0.1 it classes as a reset.
|
||||
if -(previous_value_drop_threshold) <= diff < 0:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Diff is negative, but only by a small amount therefore not a"
|
||||
" nightly reset, using previous value (%s) instead of api value"
|
||||
" (%s)"
|
||||
),
|
||||
previous_value,
|
||||
api_value,
|
||||
)
|
||||
return_value = previous_value
|
||||
else:
|
||||
_LOGGER.debug("%s - No drop detected, using API value", name)
|
||||
|
||||
# Lifetime total values should always be increasing, they will never reset,
|
||||
# however the API sometimes returns 0 values when the clock turns to 00:00
|
||||
# local time in that scenario we should just return the previous value
|
||||
# Scenarios:
|
||||
# 1 - System has a genuine 0 value when it it first commissioned:
|
||||
# - will return 0 until a non-zero value is registered
|
||||
# 2 - System has been running fine but temporarily resets to 0 briefly
|
||||
# at midnight:
|
||||
# - will return the previous value
|
||||
# 3 - HA is restarted during the midnight 'outage' - Not handled:
|
||||
# - Previous value will not exist meaning 0 will be returned
|
||||
# - This is an edge case that would be better handled by looking
|
||||
# up the previous value of the entity from the recorder
|
||||
if never_resets and api_value == 0 and previous_value:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"API value is 0, but this value should never reset, returning"
|
||||
" previous value (%s) instead"
|
||||
),
|
||||
previous_value,
|
||||
)
|
||||
return_value = previous_value
|
||||
|
||||
self.previous_values[variable] = return_value
|
||||
|
||||
return (return_value, native_unit_of_measurement)
|
||||
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Remove a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
# Remove any remaining disabled or ignored entries
|
||||
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||
|
@ -1,129 +1,11 @@
|
||||
"""Config flow for Sun WEG integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
from sunweg.api import APIHelper, SunWegApiError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import CONF_PLANT_ID, DOMAIN
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class SunWEGConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow class."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialise sun weg server flow."""
|
||||
self.api: APIHelper = None
|
||||
self.data: dict[str, Any] = {}
|
||||
|
||||
@callback
|
||||
def _async_show_user_form(self, step_id: str, errors=None) -> ConfigFlowResult:
|
||||
"""Show the form to the user."""
|
||||
default_username = ""
|
||||
if CONF_USERNAME in self.data:
|
||||
default_username = self.data[CONF_USERNAME]
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME, default=default_username): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id=step_id, data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
||||
def _set_auth_data(
|
||||
self, step: str, username: str, password: str
|
||||
) -> ConfigFlowResult | None:
|
||||
"""Set username and password."""
|
||||
if self.api:
|
||||
# Set username and password
|
||||
self.api.username = username
|
||||
self.api.password = password
|
||||
else:
|
||||
# Initialise the library with the username & password
|
||||
self.api = APIHelper(username, password)
|
||||
|
||||
try:
|
||||
if not self.api.authenticate():
|
||||
return self._async_show_user_form(step, {"base": "invalid_auth"})
|
||||
except SunWegApiError:
|
||||
return self._async_show_user_form(step, {"base": "timeout_connect"})
|
||||
|
||||
return None
|
||||
|
||||
async def async_step_user(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle the start of the config flow."""
|
||||
if not user_input:
|
||||
return self._async_show_user_form("user")
|
||||
|
||||
# Store authentication info
|
||||
self.data = user_input
|
||||
|
||||
conf_result = await self.hass.async_add_executor_job(
|
||||
self._set_auth_data,
|
||||
"user",
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
return await self.async_step_plant() if conf_result is None else conf_result
|
||||
|
||||
async def async_step_plant(self, user_input=None) -> ConfigFlowResult:
|
||||
"""Handle adding a "plant" to Home Assistant."""
|
||||
plant_list = await self.hass.async_add_executor_job(self.api.listPlants)
|
||||
|
||||
if len(plant_list) == 0:
|
||||
return self.async_abort(reason="no_plants")
|
||||
|
||||
plants = {plant.id: plant.name for plant in plant_list}
|
||||
|
||||
if user_input is None and len(plant_list) > 1:
|
||||
data_schema = vol.Schema({vol.Required(CONF_PLANT_ID): vol.In(plants)})
|
||||
|
||||
return self.async_show_form(step_id="plant", data_schema=data_schema)
|
||||
|
||||
if user_input is None and len(plant_list) == 1:
|
||||
user_input = {CONF_PLANT_ID: plant_list[0].id}
|
||||
|
||||
user_input[CONF_NAME] = plants[user_input[CONF_PLANT_ID]]
|
||||
await self.async_set_unique_id(user_input[CONF_PLANT_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
self.data.update(user_input)
|
||||
return self.async_create_entry(title=self.data[CONF_NAME], data=self.data)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauthorization request from SunWEG."""
|
||||
self.data.update(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 reauthorization flow."""
|
||||
if user_input is None:
|
||||
return self._async_show_user_form("reauth_confirm")
|
||||
|
||||
self.data.update(user_input)
|
||||
conf_result = await self.hass.async_add_executor_job(
|
||||
self._set_auth_data,
|
||||
"reauth_confirm",
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
if conf_result is not None:
|
||||
return conf_result
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=self.data
|
||||
)
|
||||
|
@ -1,25 +0,0 @@
|
||||
"""Define constants for the Sun WEG component."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
|
||||
class DeviceType(Enum):
|
||||
"""Device Type Enum."""
|
||||
|
||||
TOTAL = 1
|
||||
INVERTER = 2
|
||||
PHASE = 3
|
||||
STRING = 4
|
||||
|
||||
|
||||
CONF_PLANT_ID = "plant_id"
|
||||
|
||||
DEFAULT_PLANT_ID = 0
|
||||
|
||||
DEFAULT_NAME = "Sun WEG"
|
||||
|
||||
DOMAIN = "sunweg"
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"domain": "sunweg",
|
||||
"name": "Sun WEG",
|
||||
"codeowners": ["@rokam"],
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sunweg",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sunweg"],
|
||||
"requirements": ["sunweg==3.0.2"]
|
||||
"loggers": [],
|
||||
"requirements": []
|
||||
}
|
||||
|
@ -1,178 +0,0 @@
|
||||
"""Read status of SunWEG inverters."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from sunweg.api import APIHelper
|
||||
from sunweg.device import Inverter
|
||||
from sunweg.plant import Plant
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .. import SunWEGData
|
||||
from ..const import CONF_PLANT_ID, DEFAULT_PLANT_ID, DOMAIN, DeviceType
|
||||
from .inverter import INVERTER_SENSOR_TYPES
|
||||
from .phase import PHASE_SENSOR_TYPES
|
||||
from .sensor_entity_description import SunWEGSensorEntityDescription
|
||||
from .string import STRING_SENSOR_TYPES
|
||||
from .total import TOTAL_SENSOR_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_device_list(
|
||||
api: APIHelper, config: MappingProxyType[str, Any]
|
||||
) -> tuple[list[Inverter], int]:
|
||||
"""Retrieve the device list for the selected plant."""
|
||||
plant_id = int(config[CONF_PLANT_ID])
|
||||
|
||||
if plant_id == DEFAULT_PLANT_ID:
|
||||
plant_info: list[Plant] = api.listPlants()
|
||||
plant_id = plant_info[0].id
|
||||
|
||||
devices: list[Inverter] = []
|
||||
# Get a list of devices for specified plant to add sensors for.
|
||||
for inverter in api.plant(plant_id).inverters:
|
||||
api.complete_inverter(inverter)
|
||||
devices.append(inverter)
|
||||
return (devices, plant_id)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the SunWEG sensor."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
|
||||
probe: SunWEGData = hass.data[DOMAIN][config_entry.entry_id]
|
||||
|
||||
devices, plant_id = await hass.async_add_executor_job(
|
||||
get_device_list, probe.api, config_entry.data
|
||||
)
|
||||
|
||||
entities = [
|
||||
SunWEGInverter(
|
||||
probe,
|
||||
name=f"{name} Total",
|
||||
unique_id=f"{plant_id}-{description.key}",
|
||||
description=description,
|
||||
device_type=DeviceType.TOTAL,
|
||||
)
|
||||
for description in TOTAL_SENSOR_TYPES
|
||||
]
|
||||
|
||||
# Add sensors for each device in the specified plant.
|
||||
entities.extend(
|
||||
[
|
||||
SunWEGInverter(
|
||||
probe,
|
||||
name=f"{device.name}",
|
||||
unique_id=f"{device.sn}-{description.key}",
|
||||
description=description,
|
||||
device_type=DeviceType.INVERTER,
|
||||
inverter_id=device.id,
|
||||
)
|
||||
for device in devices
|
||||
for description in INVERTER_SENSOR_TYPES
|
||||
]
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
SunWEGInverter(
|
||||
probe,
|
||||
name=f"{device.name} {phase.name}",
|
||||
unique_id=f"{device.sn}-{phase.name}-{description.key}",
|
||||
description=description,
|
||||
inverter_id=device.id,
|
||||
device_type=DeviceType.PHASE,
|
||||
deep_name=phase.name,
|
||||
)
|
||||
for device in devices
|
||||
for phase in device.phases
|
||||
for description in PHASE_SENSOR_TYPES
|
||||
]
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
SunWEGInverter(
|
||||
probe,
|
||||
name=f"{device.name} {string.name}",
|
||||
unique_id=f"{device.sn}-{string.name}-{description.key}",
|
||||
description=description,
|
||||
inverter_id=device.id,
|
||||
device_type=DeviceType.STRING,
|
||||
deep_name=string.name,
|
||||
)
|
||||
for device in devices
|
||||
for mppt in device.mppts
|
||||
for string in mppt.strings
|
||||
for description in STRING_SENSOR_TYPES
|
||||
]
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class SunWEGInverter(SensorEntity):
|
||||
"""Representation of a SunWEG Sensor."""
|
||||
|
||||
entity_description: SunWEGSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
probe: SunWEGData,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
description: SunWEGSensorEntityDescription,
|
||||
device_type: DeviceType,
|
||||
inverter_id: int = 0,
|
||||
deep_name: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a sensor."""
|
||||
self.probe = probe
|
||||
self.entity_description = description
|
||||
self.device_type = device_type
|
||||
self.inverter_id = inverter_id
|
||||
self.deep_name = deep_name
|
||||
|
||||
self._attr_name = f"{name} {description.name}"
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_icon = (
|
||||
description.icon if description.icon is not None else "mdi:solar-power"
|
||||
)
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(probe.plant_id))},
|
||||
manufacturer="SunWEG",
|
||||
name=name,
|
||||
)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest data from the Sun WEG API and updates the state."""
|
||||
self.probe.update()
|
||||
(
|
||||
self._attr_native_value,
|
||||
self._attr_native_unit_of_measurement,
|
||||
) = self.probe.get_data(
|
||||
api_variable_key=self.entity_description.api_variable_key,
|
||||
api_variable_unit=self.entity_description.api_variable_unit,
|
||||
deep_name=self.deep_name,
|
||||
device_type=self.device_type,
|
||||
inverter_id=self.inverter_id,
|
||||
name=self.entity_description.name,
|
||||
native_unit_of_measurement=self.native_unit_of_measurement,
|
||||
never_resets=self.entity_description.never_resets,
|
||||
previous_value_drop_threshold=self.entity_description.previous_value_drop_threshold,
|
||||
)
|
@ -1,70 +0,0 @@
|
||||
"""SunWEG Sensor definitions for the Inverter type."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
|
||||
from .sensor_entity_description import SunWEGSensorEntityDescription
|
||||
|
||||
INVERTER_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
|
||||
SunWEGSensorEntityDescription(
|
||||
key="inverter_energy_today",
|
||||
name="Energy today",
|
||||
api_variable_key="_today_energy",
|
||||
api_variable_unit="_today_energy_metric",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="inverter_energy_total",
|
||||
name="Lifetime energy output",
|
||||
api_variable_key="_total_energy",
|
||||
api_variable_unit="_total_energy_metric",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
suggested_display_precision=1,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
never_resets=True,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="inverter_frequency",
|
||||
name="AC frequency",
|
||||
api_variable_key="_frequency",
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="inverter_current_wattage",
|
||||
name="Output power",
|
||||
api_variable_key="_power",
|
||||
api_variable_unit="_power_metric",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="inverter_temperature",
|
||||
name="Temperature",
|
||||
api_variable_key="_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
icon="mdi:temperature-celsius",
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="inverter_power_factor",
|
||||
name="Power Factor",
|
||||
api_variable_key="_power_factor",
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
)
|
@ -1,27 +0,0 @@
|
||||
"""SunWEG Sensor definitions for the Phase type."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential
|
||||
|
||||
from .sensor_entity_description import SunWEGSensorEntityDescription
|
||||
|
||||
PHASE_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
|
||||
SunWEGSensorEntityDescription(
|
||||
key="voltage",
|
||||
name="Voltage",
|
||||
api_variable_key="_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="amperage",
|
||||
name="Amperage",
|
||||
api_variable_key="_amperage",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
)
|
@ -1,24 +0,0 @@
|
||||
"""Sensor Entity Description for the SunWEG integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SunWEGRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
api_variable_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SunWEGSensorEntityDescription(SensorEntityDescription, SunWEGRequiredKeysMixin):
|
||||
"""Describes SunWEG sensor entity."""
|
||||
|
||||
api_variable_unit: str | None = None
|
||||
previous_value_drop_threshold: float | None = None
|
||||
never_resets: bool = False
|
||||
icon: str | None = None
|
@ -1,27 +0,0 @@
|
||||
"""SunWEG Sensor definitions for the String type."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import UnitOfElectricCurrent, UnitOfElectricPotential
|
||||
|
||||
from .sensor_entity_description import SunWEGSensorEntityDescription
|
||||
|
||||
STRING_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
|
||||
SunWEGSensorEntityDescription(
|
||||
key="voltage",
|
||||
name="Voltage",
|
||||
api_variable_key="_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="amperage",
|
||||
name="Amperage",
|
||||
api_variable_key="_amperage",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
)
|
@ -1,50 +0,0 @@
|
||||
"""SunWEG Sensor definitions for Totals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower
|
||||
|
||||
from .sensor_entity_description import SunWEGSensorEntityDescription
|
||||
|
||||
TOTAL_SENSOR_TYPES: tuple[SunWEGSensorEntityDescription, ...] = (
|
||||
SunWEGSensorEntityDescription(
|
||||
key="total_money_total",
|
||||
name="Money lifetime",
|
||||
api_variable_key="_saving",
|
||||
icon="mdi:cash",
|
||||
native_unit_of_measurement="R$",
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="total_energy_today",
|
||||
name="Energy Today",
|
||||
api_variable_key="_today_energy",
|
||||
api_variable_unit="_today_energy_metric",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="total_output_power",
|
||||
name="Output Power",
|
||||
api_variable_key="_total_power",
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="total_energy_output",
|
||||
name="Lifetime energy output",
|
||||
api_variable_key="_total_energy",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
never_resets=True,
|
||||
),
|
||||
SunWEGSensorEntityDescription(
|
||||
key="last_update",
|
||||
name="Last Update",
|
||||
api_variable_key="_last_update",
|
||||
device_class=SensorDeviceClass.DATE,
|
||||
),
|
||||
)
|
@ -1,35 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"no_plants": "No plants have been found on this account",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
||||
},
|
||||
"step": {
|
||||
"plant": {
|
||||
"data": {
|
||||
"plant_id": "Plant"
|
||||
},
|
||||
"title": "Select your plant"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"title": "Enter your Sun WEG information"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"title": "The SunWEG integration has been removed",
|
||||
"description": "The SunWEG integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with SunWEG services, [doesn't work as expected anymore, demanding daily token renew]({issue}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing SunWEG integration entries]({entries})."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ from systembridgeconnector.exceptions import (
|
||||
AuthenticationException,
|
||||
ConnectionClosedException,
|
||||
ConnectionErrorException,
|
||||
DataMissingException,
|
||||
)
|
||||
from systembridgeconnector.version import Version
|
||||
from systembridgemodels.keyboard_key import KeyboardKey
|
||||
@ -184,7 +185,7 @@ async def async_setup_entry(
|
||||
"host": entry.data[CONF_HOST],
|
||||
},
|
||||
) from exception
|
||||
except TimeoutError as exception:
|
||||
except (DataMissingException, TimeoutError) as exception:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout",
|
||||
|
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