forked from home-assistant/core
Compare commits
77 Commits
edenhaus-d
...
2025.4.0b9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 12
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.5"
|
||||
HA_SHORT_VERSION: "2025.4"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -653,7 +653,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4.6.0
|
||||
uses: actions/dependency-review-action@v4.5.0
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
|
||||
@@ -364,7 +364,6 @@ homeassistant.components.notify.*
|
||||
homeassistant.components.notion.*
|
||||
homeassistant.components.number.*
|
||||
homeassistant.components.nut.*
|
||||
homeassistant.components.ohme.*
|
||||
homeassistant.components.onboarding.*
|
||||
homeassistant.components.oncue.*
|
||||
homeassistant.components.onedrive.*
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1480,6 +1480,8 @@ 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
|
||||
|
||||
15
Dockerfile
generated
15
Dockerfile
generated
@@ -14,8 +14,21 @@ ARG QEMU_CPU
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
COPY --from=ghcr.io/alexxit/go2rtc:1.9.9 /usr/local/bin/go2rtc /bin/go2rtc
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.6.10
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"domain": "eve",
|
||||
"name": "Eve",
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
@@ -68,8 +68,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "LED bar mode",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"off": "Off",
|
||||
"co2": "Carbon dioxide",
|
||||
"pm": "Particulate matter"
|
||||
}
|
||||
},
|
||||
@@ -143,8 +143,8 @@
|
||||
"led_bar_mode": {
|
||||
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"off": "[%key:component::airgradient::entity::select::led_bar_mode::state::off%]",
|
||||
"co2": "[%key:component::airgradient::entity::select::led_bar_mode::state::co2%]",
|
||||
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"city": "City",
|
||||
"state": "State",
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
"country": "Country",
|
||||
"state": "State"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -56,12 +56,12 @@
|
||||
"sensor": {
|
||||
"pollutant_label": {
|
||||
"state": {
|
||||
"co": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"n2": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"o3": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"p1": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"p2": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"s2": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]"
|
||||
"co": "Carbon monoxide",
|
||||
"n2": "Nitrogen dioxide",
|
||||
"o3": "Ozone",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Sulfur dioxide"
|
||||
}
|
||||
},
|
||||
"pollutant_level": {
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"air_quality": {
|
||||
"name": "Air Quality mode",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]",
|
||||
"off": "Off",
|
||||
"on": "On",
|
||||
"auto": "Auto"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["boto3", "botocore", "s3transfer"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["boto3==1.37.1"]
|
||||
"requirements": ["boto3==1.34.131"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||
"requirements": ["pydroid-ipcam==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
|
||||
config_entry: ApSystemsConfigEntry
|
||||
device_version: str
|
||||
battery_system: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -69,7 +68,6 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
self.api.max_power = device_info.maxPower
|
||||
self.api.min_power = device_info.minPower
|
||||
self.device_version = device_info.devVer
|
||||
self.battery_system = device_info.isBatterySystem
|
||||
|
||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||
try:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==2.5.0"]
|
||||
"requirements": ["apsystems-ez1==2.4.0"]
|
||||
}
|
||||
|
||||
@@ -36,8 +36,6 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
|
||||
super().__init__(data)
|
||||
self._api = data.coordinator.api
|
||||
self._attr_unique_id = f"{data.device_id}_inverter_status"
|
||||
if data.coordinator.battery_system:
|
||||
self._attr_available = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update switch status and availability."""
|
||||
|
||||
@@ -60,8 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("message", "media_id"),
|
||||
@@ -76,8 +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"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("preannounce_media_id"): vol.Any(str, None),
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
),
|
||||
|
||||
@@ -180,8 +180,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self,
|
||||
message: str | None = None,
|
||||
media_id: str | None = None,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Play and show an announcement on the satellite.
|
||||
|
||||
@@ -191,8 +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 is True, a sound is played before the announcement.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
If preannounce_media_id is None, no sound is played.
|
||||
|
||||
Calls async_announce with message and media id.
|
||||
"""
|
||||
@@ -202,9 +201,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
message = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
message,
|
||||
media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
message, media_id, preannounce_media_id
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
@@ -232,8 +229,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message: str | None = None,
|
||||
start_media_id: str | None = None,
|
||||
extra_system_prompt: str | None = None,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
preannounce_media_id: str | None = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Start a conversation from the satellite.
|
||||
|
||||
@@ -243,8 +239,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
If start_media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce is True, a sound is played before the start message or media.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
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.
|
||||
"""
|
||||
@@ -261,9 +257,7 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
start_message,
|
||||
start_media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
start_message, start_media_id, preannounce_media_id
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
|
||||
@@ -15,11 +15,6 @@ announce:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
@@ -45,11 +40,6 @@ start_conversation:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
|
||||
@@ -24,13 +24,9 @@
|
||||
"name": "Media ID",
|
||||
"description": "The media ID to announce instead of using text-to-speech."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the announcement."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the announcement."
|
||||
"name": "Preannounce Media ID",
|
||||
"description": "The media ID to play before the announcement."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -50,13 +46,9 @@
|
||||
"name": "Extra system prompt",
|
||||
"description": "Provide background information to the AI about the request."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the start message or media."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
"name": "Preannounce Media ID",
|
||||
"description": "The media ID to play before the start message or media."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ async def websocket_test_connection(
|
||||
hass.async_create_background_task(
|
||||
satellite.async_internal_announce(
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
|
||||
preannounce=False,
|
||||
preannounce_media_id=None,
|
||||
),
|
||||
f"assist_satellite_connection_test_{msg['entity_id']}",
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore", "botocore"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["aiobotocore==2.21.1", "botocore==1.37.1"]
|
||||
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
|
||||
}
|
||||
|
||||
@@ -175,8 +175,7 @@ class AzureStorageBackupAgent(BackupAgent):
|
||||
"""Find a blob by backup id."""
|
||||
async for blob in self._client.list_blobs(include="metadata"):
|
||||
if (
|
||||
blob.metadata is not None
|
||||
and backup_id == blob.metadata.get("backup_id", "")
|
||||
backup_id == blob.metadata.get("backup_id", "")
|
||||
and blob.metadata.get("metadata_version") == METADATA_VERSION
|
||||
):
|
||||
return blob
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"backup_manager_state": {
|
||||
"name": "Backup Manager state",
|
||||
"name": "Backup Manager State",
|
||||
"state": {
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"idle": "Idle",
|
||||
"create_backup": "Creating a backup",
|
||||
"receive_backup": "Receiving a backup",
|
||||
"restore_backup": "Restoring a backup"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Balay virtual integration."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "balay",
|
||||
"name": "Balay",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -132,7 +132,7 @@
|
||||
"name": "Charging",
|
||||
"state": {
|
||||
"off": "Not charging",
|
||||
"on": "[%key:common::state::charging%]"
|
||||
"on": "Charging"
|
||||
}
|
||||
},
|
||||
"carbon_monoxide": {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"vehicle_status": {
|
||||
"name": "Vehicle status",
|
||||
"state": {
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"standby": "Standby",
|
||||
"vehicle_detected": "Detected",
|
||||
"ready": "Ready",
|
||||
"no_power": "No power",
|
||||
|
||||
@@ -501,16 +501,18 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
|
||||
return
|
||||
|
||||
# presets and inputs might have the same name; presets have priority
|
||||
url: str | None = None
|
||||
for input_ in self._inputs:
|
||||
if input_.text == source:
|
||||
await self._player.play_url(input_.url)
|
||||
return
|
||||
url = input_.url
|
||||
for preset in self._presets:
|
||||
if preset.name == source:
|
||||
await self._player.load_preset(preset.id)
|
||||
return
|
||||
url = preset.url
|
||||
|
||||
raise ServiceValidationError(f"Source {source} not found")
|
||||
if url is None:
|
||||
raise ServiceValidationError(f"Source {source} not found")
|
||||
|
||||
await self._player.play_url(url)
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
"""Clear players playlist."""
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-data-tools==1.26.5",
|
||||
"bluetooth-data-tools==1.26.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.37.0"
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"region": "ConnectedDrive region"
|
||||
"region": "ConnectedDrive Region"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "The email address of your MyBMW/MINI Connected account.",
|
||||
@@ -113,10 +113,10 @@
|
||||
},
|
||||
"select": {
|
||||
"ac_limit": {
|
||||
"name": "AC charging limit"
|
||||
"name": "AC Charging Limit"
|
||||
},
|
||||
"charging_mode": {
|
||||
"name": "Charging mode",
|
||||
"name": "Charging Mode",
|
||||
"state": {
|
||||
"immediate_charging": "Immediate charging",
|
||||
"delayed_charging": "Delayed charging",
|
||||
@@ -181,7 +181,7 @@
|
||||
"cooling": "Cooling",
|
||||
"heating": "Heating",
|
||||
"inactive": "Inactive",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"standby": "Standby",
|
||||
"ventilation": "Ventilation"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -142,12 +142,6 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
"""Artist of current playing media, music track only."""
|
||||
if (
|
||||
not self.client.play_state.metadata.artist
|
||||
and self.client.state.source == "IR"
|
||||
):
|
||||
# Return channel instead of artist when playing internet radio
|
||||
return self.client.play_state.metadata.station
|
||||
return self.client.play_state.metadata.artist
|
||||
|
||||
@property
|
||||
@@ -175,11 +169,6 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
"""Last time the media position was updated."""
|
||||
return self.client.position_last_updated
|
||||
|
||||
@property
|
||||
def media_channel(self) -> str | None:
|
||||
"""Channel currently playing."""
|
||||
return self.client.play_state.metadata.station
|
||||
|
||||
@property
|
||||
def is_volume_muted(self) -> bool | None:
|
||||
"""Volume mute status."""
|
||||
|
||||
@@ -2,10 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Literal, cast
|
||||
|
||||
from turbojpeg import TurboJPEG
|
||||
with suppress(Exception):
|
||||
# TurboJPEG imports numpy which may or may not work so
|
||||
# we have to guard the import here. We still want
|
||||
# to import it at top level so it gets loaded
|
||||
# in the import executor and not in the event loop.
|
||||
from turbojpeg import TurboJPEG
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Image
|
||||
|
||||
@@ -98,13 +98,13 @@
|
||||
"name": "Preset",
|
||||
"state": {
|
||||
"none": "None",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"activity": "Activity",
|
||||
"eco": "Eco",
|
||||
"away": "Away",
|
||||
"boost": "Boost",
|
||||
"comfort": "Comfort",
|
||||
"eco": "Eco",
|
||||
"sleep": "Sleep"
|
||||
"home": "[%key:common::state::home%]",
|
||||
"sleep": "Sleep",
|
||||
"activity": "Activity"
|
||||
}
|
||||
},
|
||||
"preset_modes": {
|
||||
@@ -257,7 +257,7 @@
|
||||
"selector": {
|
||||
"hvac_mode": {
|
||||
"options": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"off": "Off",
|
||||
"auto": "Auto",
|
||||
"cool": "Cool",
|
||||
"dry": "Dry",
|
||||
|
||||
@@ -127,11 +127,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
|
||||
flow_id=flow_id, user_input=tokens
|
||||
)
|
||||
|
||||
# It's a background task because it should be cancelled on shutdown and there's nothing else
|
||||
# we can do in such case. There's also no need to wait for this during setup.
|
||||
self.hass.async_create_background_task(
|
||||
await_tokens(), name="Awaiting OAuth tokens"
|
||||
)
|
||||
self.hass.async_create_task(await_tokens())
|
||||
|
||||
return authorize_url
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
import pycfdns
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -117,6 +118,8 @@ class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
"sensor": {
|
||||
"zone_status": {
|
||||
"state": {
|
||||
"open": "[%key:common::state::open%]",
|
||||
"alarm": "Alarm",
|
||||
"armed": "Armed",
|
||||
"open": "Open",
|
||||
"excluded": "Excluded",
|
||||
"faulty": "Faulty",
|
||||
"inhibited": "Inhibited",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Constructa virtual integration."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "constructa",
|
||||
"name": "Constructa",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -354,35 +354,6 @@ class ChatLog:
|
||||
if self.delta_listener:
|
||||
self.delta_listener(self, asdict(tool_result))
|
||||
|
||||
async def _async_expand_prompt_template(
|
||||
self,
|
||||
llm_context: llm.LLMContext,
|
||||
prompt: str,
|
||||
language: str,
|
||||
user_name: str | None = None,
|
||||
) -> str:
|
||||
try:
|
||||
return template.Template(prompt, self.hass).async_render(
|
||||
{
|
||||
"ha_name": self.hass.config.location_name,
|
||||
"user_name": user_name,
|
||||
"llm_context": llm_context,
|
||||
},
|
||||
parse_result=False,
|
||||
)
|
||||
except TemplateError as err:
|
||||
LOGGER.error("Error rendering prompt: %s", err)
|
||||
intent_response = intent.IntentResponse(language=language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
"Sorry, I had a problem with my template",
|
||||
)
|
||||
raise ConverseError(
|
||||
"Error rendering prompt",
|
||||
conversation_id=self.conversation_id,
|
||||
response=intent_response,
|
||||
) from err
|
||||
|
||||
async def async_update_llm_data(
|
||||
self,
|
||||
conversing_domain: str,
|
||||
@@ -438,28 +409,38 @@ class ChatLog:
|
||||
):
|
||||
user_name = user.name
|
||||
|
||||
prompt_parts = []
|
||||
prompt_parts.append(
|
||||
await self._async_expand_prompt_template(
|
||||
llm_context,
|
||||
(user_llm_prompt or llm.DEFAULT_INSTRUCTIONS_PROMPT),
|
||||
user_input.language,
|
||||
user_name,
|
||||
try:
|
||||
prompt_parts = [
|
||||
template.Template(
|
||||
llm.BASE_PROMPT
|
||||
+ (user_llm_prompt or llm.DEFAULT_INSTRUCTIONS_PROMPT),
|
||||
self.hass,
|
||||
).async_render(
|
||||
{
|
||||
"ha_name": self.hass.config.location_name,
|
||||
"user_name": user_name,
|
||||
"llm_context": llm_context,
|
||||
},
|
||||
parse_result=False,
|
||||
)
|
||||
]
|
||||
|
||||
except TemplateError as err:
|
||||
LOGGER.error("Error rendering prompt: %s", err)
|
||||
intent_response = intent.IntentResponse(language=user_input.language)
|
||||
intent_response.async_set_error(
|
||||
intent.IntentResponseErrorCode.UNKNOWN,
|
||||
"Sorry, I had a problem with my template",
|
||||
)
|
||||
)
|
||||
raise ConverseError(
|
||||
"Error rendering prompt",
|
||||
conversation_id=self.conversation_id,
|
||||
response=intent_response,
|
||||
) from err
|
||||
|
||||
if llm_api:
|
||||
prompt_parts.append(llm_api.api_prompt)
|
||||
|
||||
prompt_parts.append(
|
||||
await self._async_expand_prompt_template(
|
||||
llm_context,
|
||||
llm.BASE_PROMPT,
|
||||
user_input.language,
|
||||
user_name,
|
||||
)
|
||||
)
|
||||
|
||||
if extra_system_prompt := (
|
||||
# Take new system prompt if one was given
|
||||
user_input.extra_system_prompt or self.extra_system_prompt
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
"country": "Country"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "Email used to access your {cookidoo} account.",
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
"name": "[%key:component::cover::title%]",
|
||||
"state": {
|
||||
"open": "[%key:common::state::open%]",
|
||||
"opening": "[%key:common::state::opening%]",
|
||||
"opening": "Opening",
|
||||
"closed": "[%key:common::state::closed%]",
|
||||
"closing": "[%key:common::state::closing%]",
|
||||
"stopped": "[%key:common::state::stopped%]"
|
||||
"closing": "Closing",
|
||||
"stopped": "Stopped"
|
||||
},
|
||||
"state_attributes": {
|
||||
"current_position": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To be able to use this integration, you have to enable the following option in Deluge settings: Daemon > Allow remote controls",
|
||||
"description": "To be able to use this integration, you have to enable the following option in deluge settings: Daemon > Allow remote controls",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"],
|
||||
"requirements": ["async-upnp-client==0.43.0", "getmac==0.9.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dependencies": ["ssdp"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["async-upnp-client==0.44.0"],
|
||||
"requirements": ["async-upnp-client==0.43.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
"protect_mode": {
|
||||
"name": "Protect mode",
|
||||
"state": {
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"away": "Away",
|
||||
"home": "Home",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
operation_modes = set()
|
||||
op_list = []
|
||||
for mode in econet_modes:
|
||||
if (
|
||||
mode is not WaterHeaterOperationMode.UNKNOWN
|
||||
and mode is not WaterHeaterOperationMode.VACATION
|
||||
):
|
||||
ha_mode = ECONET_STATE_TO_HA[mode]
|
||||
operation_modes.add(ha_mode)
|
||||
return list(operation_modes)
|
||||
op_list.append(ha_mode)
|
||||
return op_list
|
||||
|
||||
@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.5.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.4.0"]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"step": {
|
||||
"auth": {
|
||||
"data": {
|
||||
"country": "[%key:common::config_flow::data::country%]",
|
||||
"country": "Country",
|
||||
"override_rest_url": "REST URL",
|
||||
"override_mqtt_url": "MQTT URL",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -100,11 +100,7 @@ class ElkEntity(Entity):
|
||||
return {"index": self._element.index + 1}
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
"""Handle changes to the element.
|
||||
|
||||
This method is called when the element changes. It should be
|
||||
overridden by subclasses to handle the changes.
|
||||
"""
|
||||
pass
|
||||
|
||||
@callback
|
||||
def _element_callback(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
@@ -115,7 +111,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_changed(self._element, {})
|
||||
self._element_callback(self._element, {})
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
||||
@@ -46,8 +46,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
type EmulatedRokuConfigEntry = ConfigEntry[EmulatedRoku]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the emulated roku component."""
|
||||
@@ -67,21 +65,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up an emulated roku server from a config entry."""
|
||||
config = entry.data
|
||||
name: str = config[CONF_NAME]
|
||||
listen_port: int = config[CONF_LISTEN_PORT]
|
||||
host_ip: str = config.get(CONF_HOST_IP) or await async_get_source_ip(hass)
|
||||
advertise_ip: str | None = config.get(CONF_ADVERTISE_IP)
|
||||
advertise_port: int | None = config.get(CONF_ADVERTISE_PORT)
|
||||
upnp_bind_multicast: bool | None = config.get(CONF_UPNP_BIND_MULTICAST)
|
||||
config = config_entry.data
|
||||
|
||||
if DOMAIN not in hass.data:
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
name = config[CONF_NAME]
|
||||
listen_port = config[CONF_LISTEN_PORT]
|
||||
host_ip = config.get(CONF_HOST_IP) or await async_get_source_ip(hass)
|
||||
advertise_ip = config.get(CONF_ADVERTISE_IP)
|
||||
advertise_port = config.get(CONF_ADVERTISE_PORT)
|
||||
upnp_bind_multicast = config.get(CONF_UPNP_BIND_MULTICAST)
|
||||
|
||||
server = EmulatedRoku(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
name,
|
||||
host_ip,
|
||||
listen_port,
|
||||
@@ -89,12 +88,14 @@ async def async_setup_entry(
|
||||
advertise_port,
|
||||
upnp_bind_multicast,
|
||||
)
|
||||
entry.runtime_data = server
|
||||
|
||||
hass.data[DOMAIN][name] = server
|
||||
|
||||
return await server.setup()
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: EmulatedRokuConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await entry.runtime_data.unload()
|
||||
name = entry.data[CONF_NAME]
|
||||
server = hass.data[DOMAIN].pop(name)
|
||||
return await server.unload()
|
||||
|
||||
@@ -5,13 +5,7 @@ import logging
|
||||
from emulated_roku import EmulatedRokuCommandHandler, EmulatedRokuServer
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
CoreState,
|
||||
Event,
|
||||
EventOrigin,
|
||||
HomeAssistant,
|
||||
)
|
||||
from homeassistant.core import CoreState, EventOrigin
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
@@ -33,18 +27,16 @@ class EmulatedRoku:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry_id: str,
|
||||
name: str,
|
||||
host_ip: str,
|
||||
listen_port: int,
|
||||
advertise_ip: str | None,
|
||||
advertise_port: int | None,
|
||||
upnp_bind_multicast: bool | None,
|
||||
) -> None:
|
||||
hass,
|
||||
name,
|
||||
host_ip,
|
||||
listen_port,
|
||||
advertise_ip,
|
||||
advertise_port,
|
||||
upnp_bind_multicast,
|
||||
):
|
||||
"""Initialize the properties."""
|
||||
self.hass = hass
|
||||
self.entry_id = entry_id
|
||||
|
||||
self.roku_usn = name
|
||||
self.host_ip = host_ip
|
||||
@@ -55,21 +47,21 @@ class EmulatedRoku:
|
||||
|
||||
self.bind_multicast = upnp_bind_multicast
|
||||
|
||||
self._api_server: EmulatedRokuServer | None = None
|
||||
self._api_server = None
|
||||
|
||||
self._unsub_start_listener: CALLBACK_TYPE | None = None
|
||||
self._unsub_stop_listener: CALLBACK_TYPE | None = None
|
||||
self._unsub_start_listener = None
|
||||
self._unsub_stop_listener = None
|
||||
|
||||
async def setup(self) -> bool:
|
||||
async def setup(self):
|
||||
"""Start the emulated_roku server."""
|
||||
|
||||
class EventCommandHandler(EmulatedRokuCommandHandler):
|
||||
"""emulated_roku command handler to turn commands into events."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
def __init__(self, hass):
|
||||
self.hass = hass
|
||||
|
||||
def on_keydown(self, roku_usn: str, key: str) -> None:
|
||||
def on_keydown(self, roku_usn, key):
|
||||
"""Handle keydown event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -81,7 +73,7 @@ class EmulatedRoku:
|
||||
EventOrigin.local,
|
||||
)
|
||||
|
||||
def on_keyup(self, roku_usn: str, key: str) -> None:
|
||||
def on_keyup(self, roku_usn, key):
|
||||
"""Handle keyup event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -93,7 +85,7 @@ class EmulatedRoku:
|
||||
EventOrigin.local,
|
||||
)
|
||||
|
||||
def on_keypress(self, roku_usn: str, key: str) -> None:
|
||||
def on_keypress(self, roku_usn, key):
|
||||
"""Handle keypress event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -105,7 +97,7 @@ class EmulatedRoku:
|
||||
EventOrigin.local,
|
||||
)
|
||||
|
||||
def launch(self, roku_usn: str, app_id: str) -> None:
|
||||
def launch(self, roku_usn, app_id):
|
||||
"""Handle launch event."""
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_ROKU_COMMAND,
|
||||
@@ -137,19 +129,17 @@ class EmulatedRoku:
|
||||
bind_multicast=self.bind_multicast,
|
||||
)
|
||||
|
||||
async def emulated_roku_stop(event: Event | None) -> None:
|
||||
async def emulated_roku_stop(event):
|
||||
"""Wrap the call to emulated_roku.close."""
|
||||
LOGGER.debug("Stopping emulated_roku %s", self.roku_usn)
|
||||
self._unsub_stop_listener = None
|
||||
assert self._api_server is not None
|
||||
await self._api_server.close()
|
||||
|
||||
async def emulated_roku_start(event: Event | None) -> None:
|
||||
async def emulated_roku_start(event):
|
||||
"""Wrap the call to emulated_roku.start."""
|
||||
try:
|
||||
LOGGER.debug("Starting emulated_roku %s", self.roku_usn)
|
||||
self._unsub_start_listener = None
|
||||
assert self._api_server is not None
|
||||
await self._api_server.start()
|
||||
except OSError:
|
||||
LOGGER.exception(
|
||||
@@ -175,7 +165,7 @@ class EmulatedRoku:
|
||||
|
||||
return True
|
||||
|
||||
async def unload(self) -> bool:
|
||||
async def unload(self):
|
||||
"""Unload the emulated_roku server."""
|
||||
LOGGER.debug("Unloading emulated_roku %s", self.roku_usn)
|
||||
|
||||
@@ -187,7 +177,6 @@ class EmulatedRoku:
|
||||
self._unsub_stop_listener()
|
||||
self._unsub_stop_listener = None
|
||||
|
||||
assert self._api_server is not None
|
||||
await self._api_server.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -25,7 +25,6 @@ from homeassistant.core import (
|
||||
split_entity_id,
|
||||
valid_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
@@ -123,10 +122,6 @@ SOURCE_ADAPTERS: Final = (
|
||||
)
|
||||
|
||||
|
||||
class EntityNotFoundError(HomeAssistantError):
|
||||
"""When a referenced entity was not found."""
|
||||
|
||||
|
||||
class SensorManager:
|
||||
"""Class to handle creation/removal of sensor data."""
|
||||
|
||||
@@ -316,25 +311,43 @@ class EnergyCostSensor(SensorEntity):
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
try:
|
||||
energy_price, energy_price_unit = self._get_energy_price(
|
||||
valid_units, default_price_unit
|
||||
# Determine energy price
|
||||
if self._config["entity_energy_price"] is not None:
|
||||
energy_price_state = self.hass.states.get(
|
||||
self._config["entity_energy_price"]
|
||||
)
|
||||
except EntityNotFoundError:
|
||||
return
|
||||
except ValueError:
|
||||
energy_price = None
|
||||
|
||||
if energy_price_state is None:
|
||||
return
|
||||
|
||||
try:
|
||||
energy_price = float(energy_price_state.state)
|
||||
except ValueError:
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities except
|
||||
# price are in place. This means that the cost will update the first
|
||||
# time the energy is updated after the price entity is in place.
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, ""
|
||||
).partition("/")[2]
|
||||
|
||||
# For backwards compatibility we don't validate the unit of the price
|
||||
# If it is not valid, we assume it's our default price unit.
|
||||
if energy_price_unit not in valid_units:
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
else:
|
||||
energy_price = cast(float, self._config["number_energy_price"])
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities are in place or
|
||||
# only the price is missing. In the later case, cost will update the first
|
||||
# time the energy is updated after the price entity is in place.
|
||||
# Initialize as it's the first time all required entities are in place.
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
if energy_price is None:
|
||||
return
|
||||
|
||||
energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if energy_unit is None or energy_unit not in valid_units:
|
||||
@@ -370,9 +383,20 @@ class EnergyCostSensor(SensorEntity):
|
||||
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||
cur_value = cast(float, self._attr_native_value)
|
||||
|
||||
converted_energy_price = self._convert_energy_price(
|
||||
energy_price, energy_price_unit, energy_unit
|
||||
)
|
||||
if energy_price_unit is None:
|
||||
converted_energy_price = energy_price
|
||||
else:
|
||||
converter: Callable[[float, str, str], float]
|
||||
if energy_unit in VALID_ENERGY_UNITS:
|
||||
converter = unit_conversion.EnergyConverter.convert
|
||||
else:
|
||||
converter = unit_conversion.VolumeConverter.convert
|
||||
|
||||
converted_energy_price = converter(
|
||||
energy_price,
|
||||
energy_unit,
|
||||
energy_price_unit,
|
||||
)
|
||||
|
||||
self._attr_native_value = (
|
||||
cur_value + (energy - old_energy_value) * converted_energy_price
|
||||
@@ -380,52 +404,6 @@ class EnergyCostSensor(SensorEntity):
|
||||
|
||||
self._last_energy_sensor_state = energy_state
|
||||
|
||||
def _get_energy_price(
|
||||
self, valid_units: set[str], default_unit: str | None
|
||||
) -> tuple[float, str | None]:
|
||||
"""Get the energy price.
|
||||
|
||||
Raises:
|
||||
EntityNotFoundError: When the energy price entity is not found.
|
||||
ValueError: When the entity state is not a valid float.
|
||||
|
||||
"""
|
||||
|
||||
if self._config["entity_energy_price"] is None:
|
||||
return cast(float, self._config["number_energy_price"]), default_unit
|
||||
|
||||
energy_price_state = self.hass.states.get(self._config["entity_energy_price"])
|
||||
if energy_price_state is None:
|
||||
raise EntityNotFoundError
|
||||
|
||||
energy_price = float(energy_price_state.state)
|
||||
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, ""
|
||||
).partition("/")[2]
|
||||
|
||||
# For backwards compatibility we don't validate the unit of the price
|
||||
# If it is not valid, we assume it's our default price unit.
|
||||
if energy_price_unit not in valid_units:
|
||||
energy_price_unit = default_unit
|
||||
|
||||
return energy_price, energy_price_unit
|
||||
|
||||
def _convert_energy_price(
|
||||
self, energy_price: float, energy_price_unit: str | None, energy_unit: str
|
||||
) -> float:
|
||||
"""Convert the energy price to the correct unit."""
|
||||
if energy_price_unit is None:
|
||||
return energy_price
|
||||
|
||||
converter: Callable[[float, str, str], float]
|
||||
if energy_unit in VALID_ENERGY_UNITS:
|
||||
converter = unit_conversion.EnergyConverter.convert
|
||||
else:
|
||||
converter = unit_conversion.VolumeConverter.convert
|
||||
|
||||
return converter(energy_price, energy_unit, energy_price_unit)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
energy_state = self.hass.states.get(self._config[self._adapter.stat_energy_key])
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "[%key:common::config_flow::data::language%]",
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
"language": "Language",
|
||||
"country": "Country"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -310,13 +310,12 @@ class EsphomeAssistSatellite(
|
||||
self.entry_data.api_version
|
||||
)
|
||||
)
|
||||
if feature_flags & VoiceAssistantFeature.SPEAKER and (
|
||||
stream := tts.async_get_stream(self.hass, tts_output["token"])
|
||||
):
|
||||
if feature_flags & VoiceAssistantFeature.SPEAKER:
|
||||
media_id = tts_output["media_id"]
|
||||
self._tts_streaming_task = (
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._stream_tts_audio(stream),
|
||||
self._stream_tts_audio(media_id),
|
||||
"esphome_voice_assistant_tts",
|
||||
)
|
||||
)
|
||||
@@ -565,7 +564,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
async def _stream_tts_audio(
|
||||
self,
|
||||
tts_result: tts.ResultStream,
|
||||
media_id: str,
|
||||
sample_rate: int = 16000,
|
||||
sample_width: int = 2,
|
||||
sample_channels: int = 1,
|
||||
@@ -580,13 +579,14 @@ class EsphomeAssistSatellite(
|
||||
if not self._is_running:
|
||||
return
|
||||
|
||||
if tts_result.extension != "wav":
|
||||
_LOGGER.error(
|
||||
"Only WAV audio can be streamed, got %s", tts_result.extension
|
||||
)
|
||||
return
|
||||
extension, data = await tts.async_get_media_source_audio(
|
||||
self.hass,
|
||||
media_id,
|
||||
)
|
||||
|
||||
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
|
||||
if extension != "wav":
|
||||
_LOGGER.error("Only WAV audio can be streamed, got %s", extension)
|
||||
return
|
||||
|
||||
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
||||
if (
|
||||
|
||||
@@ -128,23 +128,8 @@ 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:
|
||||
|
||||
@@ -13,7 +13,6 @@ from aioesphomeapi import (
|
||||
APIConnectionError,
|
||||
APIVersion,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EncryptionHelloAPIError,
|
||||
EntityInfo,
|
||||
HomeassistantServiceCall,
|
||||
InvalidAuthAPIError,
|
||||
@@ -571,7 +570,6 @@ class ESPHomeManager:
|
||||
if isinstance(
|
||||
err,
|
||||
(
|
||||
EncryptionHelloAPIError,
|
||||
RequiresEncryptionAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
InvalidAuthAPIError,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.8.0",
|
||||
"aioesphomeapi==29.7.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.12.0"
|
||||
],
|
||||
|
||||
@@ -43,9 +43,6 @@
|
||||
},
|
||||
"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,6 +193,7 @@ 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(
|
||||
@@ -200,6 +201,7 @@ 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(
|
||||
@@ -223,7 +225,6 @@ 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(
|
||||
@@ -231,15 +232,12 @@ 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(
|
||||
key="link_noise_margin_sent",
|
||||
translation_key="link_noise_margin_sent",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_noise_margin_sent_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
@@ -247,8 +245,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
key="link_noise_margin_received",
|
||||
translation_key="link_noise_margin_received",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_noise_margin_received_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
@@ -256,8 +252,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
key="link_attenuation_sent",
|
||||
translation_key="link_attenuation_sent",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_attenuation_sent_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
@@ -265,8 +259,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
key="link_attenuation_received",
|
||||
translation_key="link_attenuation_received",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_attenuation_received_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
ClimateEntity,
|
||||
@@ -39,7 +38,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_BOOST]
|
||||
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT]
|
||||
SUPPORTED_FEATURES = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
@@ -195,8 +194,6 @@ 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:
|
||||
@@ -214,8 +211,6 @@ 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:
|
||||
|
||||
@@ -182,10 +182,10 @@
|
||||
"state": {
|
||||
"startup": "Startup",
|
||||
"running": "Running",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"standby": "Standby",
|
||||
"bootloading": "Bootloading",
|
||||
"error": "Error",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"idle": "Idle",
|
||||
"ready": "Ready",
|
||||
"sleeping": "Sleeping"
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250401.0"]
|
||||
"requirements": ["home-assistant-frontend==20250328.0"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"common": {
|
||||
"data_description_password": "The Remote Admin password from the Fully Kiosk Browser app settings.",
|
||||
"data_description_ssl": "Is the Fully Kiosk app configured to require SSL for the connection?",
|
||||
"data_description_verify_ssl": "Should SSL certificates be verified? This should be off for self-signed certificates."
|
||||
"data_description_verify_ssl": "Should SSL certificartes be verified? This should be off for self-signed certificates."
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""Gaggenau virtual integration."""
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "gaggenau",
|
||||
"name": "Gaggenau",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "home_connect"
|
||||
}
|
||||
@@ -539,14 +539,10 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
return
|
||||
|
||||
assert self._cur_temp is not None and self._target_temp is not None
|
||||
|
||||
min_temp = self._target_temp - self._cold_tolerance
|
||||
max_temp = self._target_temp + self._hot_tolerance
|
||||
|
||||
too_cold = self._target_temp >= self._cur_temp + self._cold_tolerance
|
||||
too_hot = self._cur_temp >= self._target_temp + self._hot_tolerance
|
||||
if self._is_device_active:
|
||||
if (self.ac_mode and self._cur_temp <= min_temp) or (
|
||||
not self.ac_mode and self._cur_temp >= max_temp
|
||||
):
|
||||
if (self.ac_mode and too_cold) or (not self.ac_mode and too_hot):
|
||||
_LOGGER.debug("Turning off heater %s", self.heater_entity_id)
|
||||
await self._async_heater_turn_off()
|
||||
elif time is not None:
|
||||
@@ -556,9 +552,7 @@ class GenericThermostat(ClimateEntity, RestoreEntity):
|
||||
self.heater_entity_id,
|
||||
)
|
||||
await self._async_heater_turn_on()
|
||||
elif (self.ac_mode and self._cur_temp > max_temp) or (
|
||||
not self.ac_mode and self._cur_temp < min_temp
|
||||
):
|
||||
elif (self.ac_mode and too_hot) or (not self.ac_mode and too_cold):
|
||||
_LOGGER.debug("Turning on heater %s", self.heater_entity_id)
|
||||
await self._async_heater_turn_on()
|
||||
elif time is not None:
|
||||
|
||||
@@ -21,17 +21,17 @@
|
||||
"heater": "Switch entity used to cool or heat depending on A/C mode.",
|
||||
"target_sensor": "Temperature sensor that reflects the current temperature.",
|
||||
"min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.",
|
||||
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor goes below 24.5.",
|
||||
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor equals or goes below 24.5.",
|
||||
"hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5."
|
||||
}
|
||||
},
|
||||
"presets": {
|
||||
"title": "Temperature presets",
|
||||
"data": {
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"away_temp": "[%key:common::state::not_home%]",
|
||||
"away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
|
||||
"comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
|
||||
"eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]",
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]",
|
||||
"activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]"
|
||||
}
|
||||
@@ -63,10 +63,10 @@
|
||||
"presets": {
|
||||
"title": "[%key:component::generic_thermostat::config::step::presets::title%]",
|
||||
"data": {
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"away_temp": "[%key:common::state::not_home%]",
|
||||
"away_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
|
||||
"comfort_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]",
|
||||
"eco_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::eco%]",
|
||||
"home_temp": "[%key:common::state::home%]",
|
||||
"sleep_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::sleep%]",
|
||||
"activity_temp": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::activity%]"
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.1"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": [
|
||||
"google-cloud-texttospeech==2.25.1",
|
||||
"google-cloud-speech==2.31.1"
|
||||
"google-cloud-texttospeech==2.17.2",
|
||||
"google-cloud-speech==2.27.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from google import genai
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from requests.exceptions import Timeout
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -356,15 +356,6 @@ 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(
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"fix_menu": {
|
||||
"description": "Add-on {addon} is set to start at boot but failed to start. Usually this occurs when the configuration is incorrect or the same port is used in multiple add-ons. Check the configuration as well as logs for {addon} and Supervisor.\n\nUse Start to try again or Disable to turn off the start at boot option.",
|
||||
"menu_options": {
|
||||
"addon_execute_start": "[%key:common::action::start%]",
|
||||
"addon_disable_boot": "[%key:common::action::disable%]"
|
||||
"addon_execute_start": "Start",
|
||||
"addon_disable_boot": "Disable"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_QUEUE_IDS = "queue_ids"
|
||||
DOMAIN = "heos"
|
||||
ENTRY_TITLE = "HEOS System"
|
||||
SERVICE_GET_QUEUE = "get_queue"
|
||||
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||
SERVICE_GROUP_VOLUME_DOWN = "group_volume_down"
|
||||
SERVICE_GROUP_VOLUME_UP = "group_volume_up"
|
||||
SERVICE_REMOVE_FROM_QUEUE = "remove_from_queue"
|
||||
SERVICE_SIGN_IN = "sign_in"
|
||||
SERVICE_SIGN_OUT = "sign_out"
|
||||
|
||||
@@ -3,9 +3,6 @@
|
||||
"get_queue": {
|
||||
"service": "mdi:playlist-music"
|
||||
},
|
||||
"remove_from_queue": {
|
||||
"service": "mdi:playlist-remove"
|
||||
},
|
||||
"group_volume_set": {
|
||||
"service": "mdi:volume-medium"
|
||||
},
|
||||
|
||||
@@ -24,10 +24,12 @@ from pyheos import (
|
||||
const as heos_const,
|
||||
)
|
||||
from pyheos.util import mediauri as heos_source
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
@@ -41,16 +43,30 @@ from homeassistant.components.media_player import (
|
||||
)
|
||||
from homeassistant.components.media_source import BrowseMediaSource
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, callback
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_platform,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import services
|
||||
from .const import DOMAIN as HEOS_DOMAIN
|
||||
from .const import (
|
||||
DOMAIN as HEOS_DOMAIN,
|
||||
SERVICE_GET_QUEUE,
|
||||
SERVICE_GROUP_VOLUME_DOWN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
SERVICE_GROUP_VOLUME_UP,
|
||||
)
|
||||
from .coordinator import HeosConfigEntry, HeosCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -121,7 +137,25 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add media players for a config entry."""
|
||||
services.register_media_player_services()
|
||||
# Register custom entity services
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GET_QUEUE,
|
||||
None,
|
||||
"async_get_queue",
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
{vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float},
|
||||
"async_set_group_volume_level",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GROUP_VOLUME_DOWN, None, "async_group_volume_down"
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GROUP_VOLUME_UP, None, "async_group_volume_up"
|
||||
)
|
||||
|
||||
def add_entities_callback(players: Sequence[HeosPlayer]) -> None:
|
||||
"""Add entities for each player."""
|
||||
@@ -353,15 +387,6 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
await self._player.play_preset_station(index)
|
||||
return
|
||||
|
||||
if media_type == "queue":
|
||||
# media_id must be an int
|
||||
try:
|
||||
queue_id = int(media_id)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid queue id '{media_id}'") from None
|
||||
await self._player.play_queue(queue_id)
|
||||
return
|
||||
|
||||
raise ValueError(f"Unsupported media type '{media_type}'")
|
||||
|
||||
@catch_action_error("select source")
|
||||
@@ -475,10 +500,6 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
await self.coordinator.heos.set_group(new_members)
|
||||
return
|
||||
|
||||
async def async_remove_from_queue(self, queue_ids: list[int]) -> None:
|
||||
"""Remove items from the queue."""
|
||||
await self._player.remove_from_queue(queue_ids)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if the device is available."""
|
||||
|
||||
@@ -1,33 +1,19 @@
|
||||
"""Services for the HEOS integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from pyheos import CommandAuthenticationError, Heos, HeosError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import ATTR_MEDIA_VOLUME_LEVEL
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_platform,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.typing import VolDictType, VolSchemaType
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
|
||||
from .const import (
|
||||
ATTR_PASSWORD,
|
||||
ATTR_QUEUE_IDS,
|
||||
ATTR_USERNAME,
|
||||
DOMAIN,
|
||||
SERVICE_GET_QUEUE,
|
||||
SERVICE_GROUP_VOLUME_DOWN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
SERVICE_GROUP_VOLUME_UP,
|
||||
SERVICE_REMOVE_FROM_QUEUE,
|
||||
SERVICE_SIGN_IN,
|
||||
SERVICE_SIGN_OUT,
|
||||
)
|
||||
@@ -58,62 +44,6 @@ def register(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EntityServiceDescription:
|
||||
"""Describe an entity service."""
|
||||
|
||||
name: str
|
||||
method_name: str
|
||||
schema: VolDictType | VolSchemaType | None = None
|
||||
supports_response: SupportsResponse = SupportsResponse.NONE
|
||||
|
||||
def async_register(self, platform: entity_platform.EntityPlatform) -> None:
|
||||
"""Register the service with the platform."""
|
||||
platform.async_register_entity_service(
|
||||
self.name,
|
||||
self.schema,
|
||||
self.method_name,
|
||||
supports_response=self.supports_response,
|
||||
)
|
||||
|
||||
|
||||
REMOVE_FROM_QUEUE_SCHEMA: Final[VolDictType] = {
|
||||
vol.Required(ATTR_QUEUE_IDS): vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.All(cv.positive_int, vol.Range(min=1))],
|
||||
vol.Unique(),
|
||||
)
|
||||
}
|
||||
GROUP_VOLUME_SET_SCHEMA: Final[VolDictType] = {
|
||||
vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float
|
||||
}
|
||||
|
||||
MEDIA_PLAYER_ENTITY_SERVICES: Final = (
|
||||
# Player queue services
|
||||
EntityServiceDescription(
|
||||
SERVICE_GET_QUEUE, "async_get_queue", supports_response=SupportsResponse.ONLY
|
||||
),
|
||||
EntityServiceDescription(
|
||||
SERVICE_REMOVE_FROM_QUEUE, "async_remove_from_queue", REMOVE_FROM_QUEUE_SCHEMA
|
||||
),
|
||||
# Group volume services
|
||||
EntityServiceDescription(
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
"async_set_group_volume_level",
|
||||
GROUP_VOLUME_SET_SCHEMA,
|
||||
),
|
||||
EntityServiceDescription(SERVICE_GROUP_VOLUME_DOWN, "async_group_volume_down"),
|
||||
EntityServiceDescription(SERVICE_GROUP_VOLUME_UP, "async_group_volume_up"),
|
||||
)
|
||||
|
||||
|
||||
def register_media_player_services() -> None:
|
||||
"""Register media_player entity services."""
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
for service in MEDIA_PLAYER_ENTITY_SERVICES:
|
||||
service.async_register(platform)
|
||||
|
||||
|
||||
def _get_controller(hass: HomeAssistant) -> Heos:
|
||||
"""Get the HEOS controller instance."""
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -4,19 +4,6 @@ get_queue:
|
||||
integration: heos
|
||||
domain: media_player
|
||||
|
||||
remove_from_queue:
|
||||
target:
|
||||
entity:
|
||||
integration: heos
|
||||
domain: media_player
|
||||
fields:
|
||||
queue_ids:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
type: number
|
||||
|
||||
group_volume_set:
|
||||
target:
|
||||
entity:
|
||||
|
||||
@@ -90,16 +90,6 @@
|
||||
"name": "Get queue",
|
||||
"description": "Retrieves the queue of the media player."
|
||||
},
|
||||
"remove_from_queue": {
|
||||
"name": "Remove from queue",
|
||||
"description": "Removes items from the play queue.",
|
||||
"fields": {
|
||||
"queue_ids": {
|
||||
"name": "Queue IDs",
|
||||
"description": "The IDs (indexes) of the items in the queue to remove."
|
||||
}
|
||||
}
|
||||
},
|
||||
"group_volume_down": {
|
||||
"name": "Turn down group volume",
|
||||
"description": "Turns down the group volume."
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from asyncio import sleep as asyncio_sleep
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
@@ -120,11 +119,8 @@ class HomeConnectCoordinator(
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
|
||||
def remove_listener_and_invalidate_context_listeners() -> None:
|
||||
# There are cases where the remove_listener will be called
|
||||
# although it has been already removed somewhere else
|
||||
with suppress(KeyError):
|
||||
remove_listener()
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
remove_listener()
|
||||
self.__dict__.pop("context_listeners", None)
|
||||
|
||||
return remove_listener_and_invalidate_context_listeners
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"""Provides a sensor for Home Connect."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
@@ -17,7 +14,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfVolume
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
@@ -45,6 +42,7 @@ class HomeConnectSensorEntityDescription(
|
||||
):
|
||||
"""Entity Description class for sensors."""
|
||||
|
||||
default_value: str | None = None
|
||||
appliance_types: tuple[str, ...] | None = None
|
||||
fetch_unit: bool = False
|
||||
|
||||
@@ -200,6 +198,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.BSH_COMMON_EVENT_PROGRAM_ABORTED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="program_aborted",
|
||||
appliance_types=("Dishwasher", "CleaningRobot", "CookProcessor"),
|
||||
),
|
||||
@@ -207,6 +206,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.BSH_COMMON_EVENT_PROGRAM_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="program_finished",
|
||||
appliance_types=(
|
||||
"Oven",
|
||||
@@ -222,6 +222,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.BSH_COMMON_EVENT_ALARM_CLOCK_ELAPSED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="alarm_clock_elapsed",
|
||||
appliance_types=("Oven", "Cooktop"),
|
||||
),
|
||||
@@ -229,6 +230,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_OVEN_EVENT_PREHEAT_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="preheat_finished",
|
||||
appliance_types=("Oven", "Cooktop"),
|
||||
),
|
||||
@@ -236,6 +238,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_OVEN_EVENT_REGULAR_PREHEAT_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="regular_preheat_finished",
|
||||
appliance_types=("Oven",),
|
||||
),
|
||||
@@ -243,6 +246,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.LAUNDRY_CARE_DRYER_EVENT_DRYING_PROCESS_FINISHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="drying_process_finished",
|
||||
appliance_types=("Dryer",),
|
||||
),
|
||||
@@ -250,6 +254,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.DISHCARE_DISHWASHER_EVENT_SALT_NEARLY_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="salt_nearly_empty",
|
||||
appliance_types=("Dishwasher",),
|
||||
),
|
||||
@@ -257,6 +262,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.DISHCARE_DISHWASHER_EVENT_RINSE_AID_NEARLY_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="rinse_aid_nearly_empty",
|
||||
appliance_types=("Dishwasher",),
|
||||
),
|
||||
@@ -264,6 +270,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_BEAN_CONTAINER_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="bean_container_empty",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -271,6 +278,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_WATER_TANK_EMPTY,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="water_tank_empty",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -278,6 +286,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DRIP_TRAY_FULL,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="drip_tray_full",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -285,6 +294,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_KEEP_MILK_TANK_COOL,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="keep_milk_tank_cool",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -292,6 +302,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_20_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_20_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -299,6 +310,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_15_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_15_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -306,6 +318,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_10_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_10_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -313,6 +326,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DESCALING_IN_5_CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="descaling_in_5_cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -320,6 +334,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_DESCALED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_should_be_descaled",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -327,6 +342,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_OVERDUE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_descaling_overdue",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -334,6 +350,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_DESCALING_BLOCKAGE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_descaling_blockage",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -341,6 +358,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CLEANED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_should_be_cleaned",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -348,6 +366,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CLEANING_OVERDUE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_cleaning_overdue",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -355,6 +374,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN20CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in20cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -362,6 +382,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN15CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in15cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -369,6 +390,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN10CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in10cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -376,6 +398,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_CALC_N_CLEAN_IN5CUPS,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="calc_n_clean_in5cups",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -383,6 +406,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_SHOULD_BE_CALC_N_CLEANED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_should_be_calc_n_cleaned",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -390,6 +414,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_OVERDUE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_calc_n_clean_overdue",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -397,6 +422,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_COFFEE_MAKER_EVENT_DEVICE_CALC_N_CLEAN_BLOCKAGE,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="device_calc_n_clean_blockage",
|
||||
appliance_types=("CoffeeMaker",),
|
||||
),
|
||||
@@ -404,6 +430,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_FREEZER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="freezer_door_alarm",
|
||||
appliance_types=("FridgeFreezer", "Freezer"),
|
||||
),
|
||||
@@ -411,6 +438,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_DOOR_ALARM_REFRIGERATOR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="refrigerator_door_alarm",
|
||||
appliance_types=("FridgeFreezer", "Refrigerator"),
|
||||
),
|
||||
@@ -418,6 +446,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.REFRIGERATION_FRIDGE_FREEZER_EVENT_TEMPERATURE_ALARM_FREEZER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="freezer_temperature_alarm",
|
||||
appliance_types=("FridgeFreezer", "Freezer"),
|
||||
),
|
||||
@@ -425,6 +454,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_EMPTY_DUST_BOX_AND_CLEAN_FILTER,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="empty_dust_box_and_clean_filter",
|
||||
appliance_types=("CleaningRobot",),
|
||||
),
|
||||
@@ -432,6 +462,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_ROBOT_IS_STUCK,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="robot_is_stuck",
|
||||
appliance_types=("CleaningRobot",),
|
||||
),
|
||||
@@ -439,6 +470,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_EVENT_DOCKING_STATION_NOT_FOUND,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="docking_station_not_found",
|
||||
appliance_types=("CleaningRobot",),
|
||||
),
|
||||
@@ -446,6 +478,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_1_FILL_LEVEL_POOR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="poor_i_dos_1_fill_level",
|
||||
appliance_types=("Washer", "WasherDryer"),
|
||||
),
|
||||
@@ -453,6 +486,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.LAUNDRY_CARE_WASHER_EVENT_I_DOS_2_FILL_LEVEL_POOR,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="poor_i_dos_2_fill_level",
|
||||
appliance_types=("Washer", "WasherDryer"),
|
||||
),
|
||||
@@ -460,6 +494,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_NEARLY_REACHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="grease_filter_max_saturation_nearly_reached",
|
||||
appliance_types=("Hood",),
|
||||
),
|
||||
@@ -467,6 +502,7 @@ EVENT_SENSORS = (
|
||||
key=EventKey.COOKING_COMMON_EVENT_HOOD_GREASE_FILTER_MAX_SATURATION_REACHED,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=EVENT_OPTIONS,
|
||||
default_value="off",
|
||||
translation_key="grease_filter_max_saturation_reached",
|
||||
appliance_types=("Hood",),
|
||||
),
|
||||
@@ -479,6 +515,12 @@ def _get_entities_for_appliance(
|
||||
) -> list[HomeConnectEntity]:
|
||||
"""Get a list of entities."""
|
||||
return [
|
||||
*[
|
||||
HomeConnectEventSensor(entry.runtime_data, appliance, description)
|
||||
for description in EVENT_SENSORS
|
||||
if description.appliance_types
|
||||
and appliance.info.type in description.appliance_types
|
||||
],
|
||||
*[
|
||||
HomeConnectProgramSensor(entry.runtime_data, appliance, desc)
|
||||
for desc in BSH_PROGRAM_SENSORS
|
||||
@@ -492,72 +534,6 @@ def _get_entities_for_appliance(
|
||||
]
|
||||
|
||||
|
||||
def _add_event_sensor_entity(
|
||||
entry: HomeConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
appliance: HomeConnectApplianceData,
|
||||
description: HomeConnectSensorEntityDescription,
|
||||
remove_event_sensor_listener_list: list[Callable[[], None]],
|
||||
) -> None:
|
||||
"""Add an event sensor entity."""
|
||||
if (
|
||||
(appliance_data := entry.runtime_data.data.get(appliance.info.ha_id)) is None
|
||||
) or description.key not in appliance_data.events:
|
||||
return
|
||||
|
||||
for remove_listener in remove_event_sensor_listener_list:
|
||||
remove_listener()
|
||||
async_add_entities(
|
||||
[
|
||||
HomeConnectEventSensor(entry.runtime_data, appliance, description),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _add_event_sensor_listeners(
|
||||
entry: HomeConnectConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]],
|
||||
) -> None:
|
||||
for appliance in entry.runtime_data.data.values():
|
||||
if appliance.info.ha_id in remove_event_sensor_listener_dict:
|
||||
continue
|
||||
for event_sensor_description in EVENT_SENSORS:
|
||||
if appliance.info.type not in cast(
|
||||
tuple[str, ...], event_sensor_description.appliance_types
|
||||
):
|
||||
continue
|
||||
# We use a list as a kind of lazy initializer, as we can use the
|
||||
# remove_listener while we are initializing it.
|
||||
remove_event_sensor_listener_list = remove_event_sensor_listener_dict[
|
||||
appliance.info.ha_id
|
||||
]
|
||||
remove_listener = entry.runtime_data.async_add_listener(
|
||||
partial(
|
||||
_add_event_sensor_entity,
|
||||
entry,
|
||||
async_add_entities,
|
||||
appliance,
|
||||
event_sensor_description,
|
||||
remove_event_sensor_listener_list,
|
||||
),
|
||||
(appliance.info.ha_id, event_sensor_description.key),
|
||||
)
|
||||
remove_event_sensor_listener_list.append(remove_listener)
|
||||
entry.async_on_unload(remove_listener)
|
||||
|
||||
|
||||
def _remove_event_sensor_listeners_on_depaired(
|
||||
entry: HomeConnectConfigEntry,
|
||||
remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]],
|
||||
) -> None:
|
||||
registered_listeners_ha_id = set(remove_event_sensor_listener_dict)
|
||||
actual_appliances = set(entry.runtime_data.data)
|
||||
for appliance_ha_id in registered_listeners_ha_id - actual_appliances:
|
||||
for listener in remove_event_sensor_listener_dict.pop(appliance_ha_id):
|
||||
listener()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
@@ -570,32 +546,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
)
|
||||
|
||||
remove_event_sensor_listener_dict: dict[str, list[CALLBACK_TYPE]] = defaultdict(
|
||||
list
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_add_special_listener(
|
||||
partial(
|
||||
_add_event_sensor_listeners,
|
||||
entry,
|
||||
async_add_entities,
|
||||
remove_event_sensor_listener_dict,
|
||||
),
|
||||
(EventKey.BSH_COMMON_APPLIANCE_PAIRED,),
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(
|
||||
entry.runtime_data.async_add_special_listener(
|
||||
partial(
|
||||
_remove_event_sensor_listeners_on_depaired,
|
||||
entry,
|
||||
remove_event_sensor_listener_dict,
|
||||
),
|
||||
(EventKey.BSH_COMMON_APPLIANCE_DEPAIRED,),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectSensor(HomeConnectEntity, SensorEntity):
|
||||
"""Sensor class for Home Connect."""
|
||||
@@ -700,5 +650,8 @@ class HomeConnectEventSensor(HomeConnectSensor):
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the sensor's status."""
|
||||
event = self.appliance.events[cast(EventKey, self.bsh_key)]
|
||||
self._update_native_value(event.value)
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
if event:
|
||||
self._update_native_value(event.value)
|
||||
elif not self._attr_native_value:
|
||||
self._attr_native_value = self.entity_description.default_value
|
||||
|
||||
@@ -511,7 +511,7 @@
|
||||
},
|
||||
"spin_speed": {
|
||||
"options": {
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "Off",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "700 rpm",
|
||||
@@ -521,7 +521,7 @@
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "1200 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "1400 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "1600 rpm",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "Off",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "Low",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "Medium",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "High"
|
||||
@@ -529,7 +529,7 @@
|
||||
},
|
||||
"vario_perfect": {
|
||||
"options": {
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:common::state::off%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "Off",
|
||||
"laundry_care_common_enum_type_vario_perfect_eco_perfect": "Eco perfect",
|
||||
"laundry_care_common_enum_type_vario_perfect_speed_perfect": "Speed perfect"
|
||||
}
|
||||
@@ -1494,7 +1494,7 @@
|
||||
"spin_speed": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_spin_speed::name%]",
|
||||
"state": {
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_700": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_700%]",
|
||||
@@ -1504,7 +1504,7 @@
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1200": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1200%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1400": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1400%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_r_p_m_1600": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_r_p_m_1600%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:common::state::off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_off": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_off%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_low": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_low%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_medium": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_medium%]",
|
||||
"laundry_care_washer_enum_type_spin_speed_ul_high": "[%key:component::home_connect::selector::spin_speed::options::laundry_care_washer_enum_type_spin_speed_ul_high%]"
|
||||
@@ -1513,7 +1513,7 @@
|
||||
"vario_perfect": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_vario_perfect::name%]",
|
||||
"state": {
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:common::state::off%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_off%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_eco_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_eco_perfect%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_speed_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_speed_perfect%]"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ from .util import (
|
||||
OwningIntegration,
|
||||
get_otbr_addon_manager,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
guess_firmware_info,
|
||||
guess_hardware_owners,
|
||||
probe_silabs_firmware_info,
|
||||
)
|
||||
@@ -512,16 +511,6 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm a discovery."""
|
||||
assert self._device is not None
|
||||
fw_info = await guess_firmware_info(self.hass, self._device)
|
||||
|
||||
# If our guess for the firmware type is actually running, we can save the user
|
||||
# an unnecessary confirmation and silently confirm the flow
|
||||
for owner in fw_info.owners:
|
||||
if await owner.is_running(self.hass):
|
||||
self._probed_firmware_info = fw_info
|
||||
return self._async_flow_finished()
|
||||
|
||||
return await self.async_step_pick_firmware()
|
||||
|
||||
|
||||
|
||||
@@ -95,7 +95,8 @@ class BaseFirmwareUpdateEntity(
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||
)
|
||||
_attr_has_entity_name = True
|
||||
# Until this entity can be associated with a device, we must manually name it
|
||||
_attr_has_entity_name = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -194,6 +195,10 @@ class BaseFirmwareUpdateEntity(
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
|
||||
# This entity is not currently associated with a device so we must manually
|
||||
# give it a name
|
||||
self._attr_name = f"{self._config_entry.title} Update"
|
||||
self._attr_title = self.entity_description.firmware_name or "Unknown"
|
||||
|
||||
if (
|
||||
|
||||
@@ -3,81 +3,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
async_register_port_event_callback,
|
||||
scan_serial_ports,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
DESCRIPTION,
|
||||
DEVICE,
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
PID,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
VID,
|
||||
)
|
||||
from .const import DESCRIPTION, DEVICE, FIRMWARE, FIRMWARE_VERSION, PRODUCT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the ZBT-1 integration."""
|
||||
|
||||
@callback
|
||||
def async_port_event_callback(
|
||||
added: set[USBDevice], removed: set[USBDevice]
|
||||
) -> None:
|
||||
"""Handle USB port events."""
|
||||
current_entries_by_path = {
|
||||
entry.data[DEVICE]: entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
}
|
||||
|
||||
for device in added | removed:
|
||||
path = device.device
|
||||
entry = current_entries_by_path.get(path)
|
||||
|
||||
if entry is not None:
|
||||
_LOGGER.debug(
|
||||
"Device %r has changed state, reloading config entry %s",
|
||||
path,
|
||||
entry,
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async_register_port_event_callback(hass, async_port_event_callback)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant SkyConnect config entry."""
|
||||
|
||||
# Postpone loading the config entry if the device is missing
|
||||
device_path = entry.data[DEVICE]
|
||||
if not await hass.async_add_executor_job(os.path.exists, device_path):
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -91,7 +29,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
"""Migrate old entry."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
|
||||
"Migrating from version %s:%s", config_entry.version, config_entry.minor_version
|
||||
)
|
||||
|
||||
if config_entry.version == 1:
|
||||
@@ -126,43 +64,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
if config_entry.minor_version == 3:
|
||||
# Old SkyConnect config entries were missing keys
|
||||
if any(
|
||||
key not in config_entry.data
|
||||
for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER)
|
||||
):
|
||||
serial_ports = await hass.async_add_executor_job(scan_serial_ports)
|
||||
serial_ports_info = {port.device: port for port in serial_ports}
|
||||
device = config_entry.data[DEVICE]
|
||||
|
||||
if not (usb_info := serial_ports_info.get(device)):
|
||||
raise HomeAssistantError(
|
||||
f"USB device {device} is missing, cannot migrate"
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
VID: usb_info.vid,
|
||||
PID: usb_info.pid,
|
||||
MANUFACTURER: usb_info.manufacturer,
|
||||
PRODUCT: usb_info.description,
|
||||
DESCRIPTION: usb_info.description,
|
||||
SERIAL_NUMBER: usb_info.serial_number,
|
||||
},
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
else:
|
||||
# Existing entries are migrated by just incrementing the version
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
config_entry.version,
|
||||
|
||||
@@ -81,7 +81,7 @@ class HomeAssistantSkyConnectConfigFlow(
|
||||
"""Handle a config flow for Home Assistant SkyConnect."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 4
|
||||
MINOR_VERSION = 3
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize the config flow."""
|
||||
|
||||
@@ -195,10 +195,5 @@
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""SkyConnect firmware update entity."""
|
||||
|
||||
bootloader_reset_type = None
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"radio_firmware": {
|
||||
"firmware": {
|
||||
"name": "Radio firmware"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
] = {
|
||||
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -56,7 +55,6 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -67,8 +65,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -79,8 +76,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -92,7 +88,6 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@@ -173,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,
|
||||
|
||||
@@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.COVER,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
"""The Homee climate platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType, NodeProfile
|
||||
from pyHomee.model import HomeeNode
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRESET_BOOST,
|
||||
PRESET_ECO,
|
||||
PRESET_NONE,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .const import CLIMATE_PROFILES, DOMAIN, HOMEE_UNIT_TO_HA_UNIT, PRESET_MANUAL
|
||||
from .entity import HomeeNodeEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
ROOM_THERMOSTATS = {
|
||||
NodeProfile.ROOM_THERMOSTAT,
|
||||
NodeProfile.ROOM_THERMOSTAT_WITH_HUMIDITY_SENSOR,
|
||||
NodeProfile.WIFI_ROOM_THERMOSTAT,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the climate component."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeClimate(node, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
if node.profile in CLIMATE_PROFILES
|
||||
)
|
||||
|
||||
|
||||
class HomeeClimate(HomeeNodeEntity, ClimateEntity):
|
||||
"""Representation of a Homee climate entity."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_translation_key = DOMAIN
|
||||
|
||||
def __init__(self, node: HomeeNode, entry: HomeeConfigEntry) -> None:
|
||||
"""Initialize a Homee climate entity."""
|
||||
super().__init__(node, entry)
|
||||
|
||||
(
|
||||
self._attr_supported_features,
|
||||
self._attr_hvac_modes,
|
||||
self._attr_preset_modes,
|
||||
) = get_climate_features(self._node)
|
||||
|
||||
self._target_temp = self._node.get_attribute_by_type(
|
||||
AttributeType.TARGET_TEMPERATURE
|
||||
)
|
||||
assert self._target_temp is not None
|
||||
self._attr_temperature_unit = str(HOMEE_UNIT_TO_HA_UNIT[self._target_temp.unit])
|
||||
self._attr_target_temperature_step = self._target_temp.step_value
|
||||
self._attr_unique_id = f"{self._attr_unique_id}-{self._target_temp.id}"
|
||||
|
||||
self._heating_mode = self._node.get_attribute_by_type(
|
||||
AttributeType.HEATING_MODE
|
||||
)
|
||||
self._temperature = self._node.get_attribute_by_type(AttributeType.TEMPERATURE)
|
||||
self._valve_position = self._node.get_attribute_by_type(
|
||||
AttributeType.CURRENT_VALVE_POSITION
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return the hvac operation mode."""
|
||||
if ClimateEntityFeature.TURN_OFF in self.supported_features and (
|
||||
self._heating_mode is not None
|
||||
):
|
||||
if self._heating_mode.current_value == 0:
|
||||
return HVACMode.OFF
|
||||
|
||||
return HVACMode.HEAT
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return the hvac action."""
|
||||
if self._heating_mode is not None and self._heating_mode.current_value == 0:
|
||||
return HVACAction.OFF
|
||||
|
||||
if (
|
||||
self._valve_position is not None and self._valve_position.current_value == 0
|
||||
) or (
|
||||
self._temperature is not None
|
||||
and self._temperature.current_value >= self.target_temperature
|
||||
):
|
||||
return HVACAction.IDLE
|
||||
|
||||
return HVACAction.HEATING
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str:
|
||||
"""Return the present preset mode."""
|
||||
if (
|
||||
ClimateEntityFeature.PRESET_MODE in self.supported_features
|
||||
and self._heating_mode is not None
|
||||
and self._heating_mode.current_value > 0
|
||||
):
|
||||
assert self._attr_preset_modes is not None
|
||||
return self._attr_preset_modes[int(self._heating_mode.current_value) - 1]
|
||||
|
||||
return PRESET_NONE
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if self._temperature is not None:
|
||||
return self._temperature.current_value
|
||||
return None
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Return the temperature we try to reach."""
|
||||
assert self._target_temp is not None
|
||||
return self._target_temp.current_value
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the lowest settable target temperature."""
|
||||
assert self._target_temp is not None
|
||||
return self._target_temp.minimum
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the lowest settable target temperature."""
|
||||
assert self._target_temp is not None
|
||||
return self._target_temp.maximum
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target hvac mode."""
|
||||
# Currently only HEAT and OFF are supported.
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, float(hvac_mode == HVACMode.HEAT)
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
assert self._heating_mode is not None and self._attr_preset_modes is not None
|
||||
await self.async_set_homee_value(
|
||||
self._heating_mode, self._attr_preset_modes.index(preset_mode) + 1
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
assert self._target_temp is not None
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
await self.async_set_homee_value(
|
||||
self._target_temp, kwargs[ATTR_TEMPERATURE]
|
||||
)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(self._heating_mode, 1)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
assert self._heating_mode is not None
|
||||
await self.async_set_homee_value(self._heating_mode, 0)
|
||||
|
||||
|
||||
def get_climate_features(
|
||||
node: HomeeNode,
|
||||
) -> tuple[ClimateEntityFeature, list[HVACMode], list[str] | None]:
|
||||
"""Determine supported climate features of a node based on the available attributes."""
|
||||
features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
hvac_modes = [HVACMode.HEAT]
|
||||
preset_modes: list[str] = []
|
||||
|
||||
if (
|
||||
attribute := node.get_attribute_by_type(AttributeType.HEATING_MODE)
|
||||
) is not None:
|
||||
features |= ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||
hvac_modes.append(HVACMode.OFF)
|
||||
|
||||
if attribute.maximum > 1:
|
||||
# Node supports more modes than off and heating.
|
||||
features |= ClimateEntityFeature.PRESET_MODE
|
||||
preset_modes.extend([PRESET_ECO, PRESET_BOOST, PRESET_MANUAL])
|
||||
|
||||
if len(preset_modes) > 0:
|
||||
preset_modes.insert(0, PRESET_NONE)
|
||||
return (features, hvac_modes, preset_modes if len(preset_modes) > 0 else None)
|
||||
@@ -95,6 +95,3 @@ LIGHT_PROFILES = [
|
||||
NodeProfile.WIFI_DIMMABLE_LIGHT,
|
||||
NodeProfile.WIFI_ON_OFF_DIMMABLE_METERING_SWITCH,
|
||||
]
|
||||
|
||||
# Climate Presets
|
||||
PRESET_MANUAL = "manual"
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"homee": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"manual": "mdi:hand-back-left"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"brightness": {
|
||||
"default": "mdi:brightness-5"
|
||||
|
||||
@@ -131,17 +131,6 @@
|
||||
"name": "Ventilate"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"homee": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"manual": "Manual"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"light_instance": {
|
||||
"name": "Light {instance}"
|
||||
@@ -308,8 +297,8 @@
|
||||
"open": "[%key:common::state::open%]",
|
||||
"closed": "[%key:common::state::closed%]",
|
||||
"partial": "Partially open",
|
||||
"opening": "[%key:common::state::opening%]",
|
||||
"closing": "[%key:common::state::closing%]"
|
||||
"opening": "Opening",
|
||||
"closing": "Closing"
|
||||
}
|
||||
},
|
||||
"uv": {
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.13"],
|
||||
"requirements": ["aiohomekit==3.2.8"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"air_purifier_state_current": {
|
||||
"state": {
|
||||
"inactive": "Inactive",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"idle": "Idle",
|
||||
"purifying": "Purifying"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_controller_id": {
|
||||
"message": "Invalid controller ID \"{controller_id}\", expected one of \"{controller_ids}\""
|
||||
"message": "Invalid controller_id \"{controller_id}\", expected one of \"{controller_ids}\""
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"hold": "Hold",
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"away": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::away%]",
|
||||
"none": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::none%]"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,34 +3,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Final
|
||||
|
||||
from aiohttp import hdrs
|
||||
from aiohttp.web import Application, Request, StreamResponse, middleware
|
||||
from aiohttp.web_exceptions import HTTPException
|
||||
from multidict import CIMultiDict, istr
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
REFERRER_POLICY: Final[istr] = istr("Referrer-Policy")
|
||||
X_CONTENT_TYPE_OPTIONS: Final[istr] = istr("X-Content-Type-Options")
|
||||
X_FRAME_OPTIONS: Final[istr] = istr("X-Frame-Options")
|
||||
|
||||
|
||||
@callback
|
||||
def setup_headers(app: Application, use_x_frame_options: bool) -> None:
|
||||
"""Create headers middleware for the app."""
|
||||
|
||||
added_headers = CIMultiDict(
|
||||
{
|
||||
REFERRER_POLICY: "no-referrer",
|
||||
X_CONTENT_TYPE_OPTIONS: "nosniff",
|
||||
hdrs.SERVER: "", # Empty server header, to prevent aiohttp of setting one.
|
||||
}
|
||||
)
|
||||
added_headers = {
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Server": "", # Empty server header, to prevent aiohttp of setting one.
|
||||
}
|
||||
|
||||
if use_x_frame_options:
|
||||
added_headers[X_FRAME_OPTIONS] = "SAMEORIGIN"
|
||||
added_headers["X-Frame-Options"] = "SAMEORIGIN"
|
||||
|
||||
@middleware
|
||||
async def headers_middleware(
|
||||
|
||||
@@ -197,11 +197,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_effect_none": {
|
||||
"title": "Light turned on with deprecated effect",
|
||||
"description": "A light was turned on with the deprecated effect `None`. This has been replaced with `off`. Please update any automations, scenes, or scripts that use this effect."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.components.light import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from ..bridge import HueBridge
|
||||
@@ -45,9 +44,6 @@ FALLBACK_MIN_KELVIN = 6500
|
||||
FALLBACK_MAX_KELVIN = 2000
|
||||
FALLBACK_KELVIN = 5800 # halfway
|
||||
|
||||
# HA 2025.4 replaced the deprecated effect "None" with HA default "off"
|
||||
DEPRECATED_EFFECT_NONE = "None"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -237,23 +233,6 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
self._color_temp_active = color_temp is not None
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||
if effect_str == DEPRECATED_EFFECT_NONE:
|
||||
# deprecated effect "None" is now "off"
|
||||
effect_str = EFFECT_OFF
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"deprecated_effect_none",
|
||||
breaks_in_ha_version="2025.10.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_effect_none",
|
||||
)
|
||||
self.logger.warning(
|
||||
"Detected deprecated effect 'None' in %s, use 'off' instead. "
|
||||
"This will stop working in HA 2025.10",
|
||||
self.entity_id,
|
||||
)
|
||||
if effect_str == EFFECT_OFF:
|
||||
# ignore effect if set to "off" and we have no effect active
|
||||
# the special effect "off" is only used to stop an active effect
|
||||
|
||||
@@ -63,14 +63,14 @@
|
||||
"name": "Mode",
|
||||
"state": {
|
||||
"normal": "Normal",
|
||||
"home": "[%key:common::state::home%]",
|
||||
"away": "[%key:common::state::not_home%]",
|
||||
"auto": "Auto",
|
||||
"baby": "Baby",
|
||||
"eco": "Eco",
|
||||
"away": "Away",
|
||||
"boost": "Boost",
|
||||
"comfort": "Comfort",
|
||||
"eco": "Eco",
|
||||
"sleep": "Sleep"
|
||||
"home": "[%key:common::state::home%]",
|
||||
"sleep": "Sleep",
|
||||
"auto": "Auto",
|
||||
"baby": "Baby"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user