forked from home-assistant/core
Compare commits
16 Commits
2025.3.0b5
...
2025.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97cc3984c5 | ||
|
|
98e317dd55 | ||
|
|
ed088aa72f | ||
|
|
51162320cb | ||
|
|
b88eab8ba3 | ||
|
|
6c080ee650 | ||
|
|
8056b0df2b | ||
|
|
3f94b7a61c | ||
|
|
1484e46317 | ||
|
|
2812c8a993 | ||
|
|
5043e2ad10 | ||
|
|
2c2fd76270 | ||
|
|
7001f8daaf | ||
|
|
b41fc932c5 | ||
|
|
0872243297 | ||
|
|
bba889975a |
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.2.26"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.5"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["webhook"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aioecowitt==2024.2.1"]
|
||||
"requirements": ["aioecowitt==2025.3.1"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250228.0"]
|
||||
"requirements": ["home-assistant-frontend==20250305.0"]
|
||||
}
|
||||
|
||||
@@ -110,7 +110,9 @@ class ThinQClimateEntity(ThinQEntity, ClimateEntity):
|
||||
self._attr_hvac_modes = [HVACMode.OFF]
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_preset_modes = []
|
||||
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
self._attr_temperature_unit = (
|
||||
self._get_unit_of_measurement(self.data.unit) or UnitOfTemperature.CELSIUS
|
||||
)
|
||||
self._requested_hvac_mode: str | None = None
|
||||
|
||||
# Set up HVAC modes.
|
||||
@@ -182,6 +184,11 @@ class ThinQClimateEntity(ThinQEntity, ClimateEntity):
|
||||
self._attr_target_temperature_high = self.data.target_temp_high
|
||||
self._attr_target_temperature_low = self.data.target_temp_low
|
||||
|
||||
# Update unit.
|
||||
self._attr_temperature_unit = (
|
||||
self._get_unit_of_measurement(self.data.unit) or UnitOfTemperature.CELSIUS
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] update status: c:%s, t:%s, l:%s, h:%s, hvac:%s, unit:%s, step:%s",
|
||||
self.coordinator.device_name,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
from datetime import timedelta
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
|
||||
# Config flow
|
||||
DOMAIN = "lg_thinq"
|
||||
COMPANY = "LGE"
|
||||
@@ -18,3 +20,10 @@ MQTT_SUBSCRIPTION_INTERVAL: Final = timedelta(days=1)
|
||||
# MQTT: Message types
|
||||
DEVICE_PUSH_MESSAGE: Final = "DEVICE_PUSH"
|
||||
DEVICE_STATUS_MESSAGE: Final = "DEVICE_STATUS"
|
||||
|
||||
# Unit conversion map
|
||||
DEVICE_UNIT_TO_HA: dict[str, str] = {
|
||||
"F": UnitOfTemperature.FAHRENHEIT,
|
||||
"C": UnitOfTemperature.CELSIUS,
|
||||
}
|
||||
REVERSE_DEVICE_UNIT_TO_HA = {v: k for k, v in DEVICE_UNIT_TO_HA.items()}
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from thinqconnect import ThinQAPIException
|
||||
from thinqconnect.integration import HABridge
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import EVENT_CORE_CONFIG_UPDATE
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ThinqConfigEntry
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, REVERSE_DEVICE_UNIT_TO_HA
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,6 +56,40 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
f"{self.device_id}_{self.sub_id}" if self.sub_id else self.device_id
|
||||
)
|
||||
|
||||
# Set your preferred temperature unit. This will allow us to retrieve
|
||||
# temperature values from the API in a converted value corresponding to
|
||||
# preferred unit.
|
||||
self._update_preferred_temperature_unit()
|
||||
|
||||
# Add a callback to handle core config update.
|
||||
self.unit_system: str | None = None
|
||||
self.hass.bus.async_listen(
|
||||
event_type=EVENT_CORE_CONFIG_UPDATE,
|
||||
listener=self._handle_update_config,
|
||||
event_filter=self.async_config_update_filter,
|
||||
)
|
||||
|
||||
async def _handle_update_config(self, _: Event) -> None:
|
||||
"""Handle update core config."""
|
||||
self._update_preferred_temperature_unit()
|
||||
|
||||
await self.async_refresh()
|
||||
|
||||
@callback
|
||||
def async_config_update_filter(self, event_data: Mapping[str, Any]) -> bool:
|
||||
"""Filter out unwanted events."""
|
||||
if (unit_system := event_data.get("unit_system")) != self.unit_system:
|
||||
self.unit_system = unit_system
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _update_preferred_temperature_unit(self) -> None:
|
||||
"""Update preferred temperature unit."""
|
||||
self.api.set_preferred_temperature_unit(
|
||||
REVERSE_DEVICE_UNIT_TO_HA.get(self.hass.config.units.temperature_unit)
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Request to the server to update the status from full response data."""
|
||||
try:
|
||||
|
||||
@@ -10,25 +10,19 @@ from thinqconnect import ThinQAPIException
|
||||
from thinqconnect.devices.const import Location
|
||||
from thinqconnect.integration import PropertyState
|
||||
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import COMPANY, DOMAIN
|
||||
from .const import COMPANY, DEVICE_UNIT_TO_HA, DOMAIN
|
||||
from .coordinator import DeviceDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
EMPTY_STATE = PropertyState()
|
||||
|
||||
UNIT_CONVERSION_MAP: dict[str, str] = {
|
||||
"F": UnitOfTemperature.FAHRENHEIT,
|
||||
"C": UnitOfTemperature.CELSIUS,
|
||||
}
|
||||
|
||||
|
||||
class ThinQEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]):
|
||||
"""The base implementation of all lg thinq entities."""
|
||||
@@ -75,7 +69,7 @@ class ThinQEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]):
|
||||
if unit is None:
|
||||
return None
|
||||
|
||||
return UNIT_CONVERSION_MAP.get(unit)
|
||||
return DEVICE_UNIT_TO_HA.get(unit)
|
||||
|
||||
def _update_status(self) -> None:
|
||||
"""Update status itself.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "matter",
|
||||
"name": "Matter (BETA)",
|
||||
"name": "Matter",
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@home-assistant/matter"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nexia"],
|
||||
"requirements": ["nexia==2.1.1"]
|
||||
"requirements": ["nexia==2.2.1"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.0.12"]
|
||||
"requirements": ["onedrive-personal-sdk==0.0.13"]
|
||||
}
|
||||
|
||||
@@ -83,13 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
# Get a Coordinator if the device is available or if we have connected to the device before
|
||||
coordinators = await asyncio.gather(
|
||||
*build_setup_functions(
|
||||
hass,
|
||||
entry,
|
||||
device_map,
|
||||
user_data,
|
||||
product_info,
|
||||
home_data.rooms,
|
||||
api_client,
|
||||
hass, entry, device_map, user_data, product_info, home_data.rooms
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
@@ -141,7 +135,6 @@ def build_setup_functions(
|
||||
user_data: UserData,
|
||||
product_info: dict[str, HomeDataProduct],
|
||||
home_data_rooms: list[HomeDataRoom],
|
||||
api_client: RoborockApiClient,
|
||||
) -> list[
|
||||
Coroutine[
|
||||
Any,
|
||||
@@ -158,7 +151,6 @@ def build_setup_functions(
|
||||
device,
|
||||
product_info[device.product_id],
|
||||
home_data_rooms,
|
||||
api_client,
|
||||
)
|
||||
for device in device_map.values()
|
||||
]
|
||||
@@ -171,12 +163,11 @@ async def setup_device(
|
||||
device: HomeDataDevice,
|
||||
product_info: HomeDataProduct,
|
||||
home_data_rooms: list[HomeDataRoom],
|
||||
api_client: RoborockApiClient,
|
||||
) -> RoborockDataUpdateCoordinator | RoborockDataUpdateCoordinatorA01 | None:
|
||||
"""Set up a coordinator for a given device."""
|
||||
if device.pv == "1.0":
|
||||
return await setup_device_v1(
|
||||
hass, entry, user_data, device, product_info, home_data_rooms, api_client
|
||||
hass, entry, user_data, device, product_info, home_data_rooms
|
||||
)
|
||||
if device.pv == "A01":
|
||||
return await setup_device_a01(hass, entry, user_data, device, product_info)
|
||||
@@ -196,7 +187,6 @@ async def setup_device_v1(
|
||||
device: HomeDataDevice,
|
||||
product_info: HomeDataProduct,
|
||||
home_data_rooms: list[HomeDataRoom],
|
||||
api_client: RoborockApiClient,
|
||||
) -> RoborockDataUpdateCoordinator | None:
|
||||
"""Set up a device Coordinator."""
|
||||
mqtt_client = await hass.async_add_executor_job(
|
||||
@@ -218,15 +208,7 @@ async def setup_device_v1(
|
||||
await mqtt_client.async_release()
|
||||
raise
|
||||
coordinator = RoborockDataUpdateCoordinator(
|
||||
hass,
|
||||
entry,
|
||||
device,
|
||||
networking,
|
||||
product_info,
|
||||
mqtt_client,
|
||||
home_data_rooms,
|
||||
api_client,
|
||||
user_data,
|
||||
hass, entry, device, networking, product_info, mqtt_client, home_data_rooms
|
||||
)
|
||||
try:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -36,7 +36,6 @@ PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.IMAGE,
|
||||
Platform.NUMBER,
|
||||
Platform.SCENE,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
|
||||
@@ -10,26 +10,17 @@ import logging
|
||||
from propcache.api import cached_property
|
||||
from roborock import HomeDataRoom
|
||||
from roborock.code_mappings import RoborockCategory
|
||||
from roborock.containers import (
|
||||
DeviceData,
|
||||
HomeDataDevice,
|
||||
HomeDataProduct,
|
||||
HomeDataScene,
|
||||
NetworkInfo,
|
||||
UserData,
|
||||
)
|
||||
from roborock.containers import DeviceData, HomeDataDevice, HomeDataProduct, NetworkInfo
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol
|
||||
from roborock.roborock_typing import DeviceProp
|
||||
from roborock.version_1_apis.roborock_local_client_v1 import RoborockLocalClientV1
|
||||
from roborock.version_1_apis.roborock_mqtt_client_v1 import RoborockMqttClientV1
|
||||
from roborock.version_a01_apis import RoborockClientA01
|
||||
from roborock.web_api import RoborockApiClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_CONNECTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -76,8 +67,6 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
product_info: HomeDataProduct,
|
||||
cloud_api: RoborockMqttClientV1,
|
||||
home_data_rooms: list[HomeDataRoom],
|
||||
api_client: RoborockApiClient,
|
||||
user_data: UserData,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -100,7 +89,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
self.cloud_api = cloud_api
|
||||
self.device_info = DeviceInfo(
|
||||
name=self.roborock_device_info.device.name,
|
||||
identifiers={(DOMAIN, self.duid)},
|
||||
identifiers={(DOMAIN, self.roborock_device_info.device.duid)},
|
||||
manufacturer="Roborock",
|
||||
model=self.roborock_device_info.product.model,
|
||||
model_id=self.roborock_device_info.product.model,
|
||||
@@ -114,10 +103,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
self.maps: dict[int, RoborockMapInfo] = {}
|
||||
self._home_data_rooms = {str(room.id): room.name for room in home_data_rooms}
|
||||
self.map_storage = RoborockMapStorage(
|
||||
hass, self.config_entry.entry_id, self.duid_slug
|
||||
hass, self.config_entry.entry_id, slugify(self.duid)
|
||||
)
|
||||
self._user_data = user_data
|
||||
self._api_client = api_client
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
@@ -147,7 +134,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
except RoborockException:
|
||||
_LOGGER.warning(
|
||||
"Using the cloud API for device %s. This is not recommended as it can lead to rate limiting. We recommend making your vacuum accessible by your Home Assistant instance",
|
||||
self.duid,
|
||||
self.roborock_device_info.device.duid,
|
||||
)
|
||||
await self.api.async_disconnect()
|
||||
# We use the cloud api if the local api fails to connect.
|
||||
@@ -207,34 +194,6 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
for room in room_mapping or ()
|
||||
}
|
||||
|
||||
async def get_scenes(self) -> list[HomeDataScene]:
|
||||
"""Get scenes."""
|
||||
try:
|
||||
return await self._api_client.get_scenes(self._user_data, self.duid)
|
||||
except RoborockException as err:
|
||||
_LOGGER.error("Failed to get scenes %s", err)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "get_scenes",
|
||||
},
|
||||
) from err
|
||||
|
||||
async def execute_scene(self, scene_id: int) -> None:
|
||||
"""Execute scene."""
|
||||
try:
|
||||
await self._api_client.execute_scene(self._user_data, scene_id)
|
||||
except RoborockException as err:
|
||||
_LOGGER.error("Failed to execute scene %s %s", scene_id, err)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "execute_scene",
|
||||
},
|
||||
) from err
|
||||
|
||||
@cached_property
|
||||
def duid(self) -> str:
|
||||
"""Get the unique id of the device as specified by Roborock."""
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""Support for Roborock scene."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.scene import Scene as SceneEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import RoborockConfigEntry
|
||||
from .coordinator import RoborockDataUpdateCoordinator
|
||||
from .entity import RoborockEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: RoborockConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up scene platform."""
|
||||
scene_lists = await asyncio.gather(
|
||||
*[coordinator.get_scenes() for coordinator in config_entry.runtime_data.v1],
|
||||
)
|
||||
async_add_entities(
|
||||
RoborockSceneEntity(
|
||||
coordinator,
|
||||
EntityDescription(
|
||||
key=str(scene.id),
|
||||
name=scene.name,
|
||||
),
|
||||
)
|
||||
for coordinator, scenes in zip(
|
||||
config_entry.runtime_data.v1, scene_lists, strict=True
|
||||
)
|
||||
for scene in scenes
|
||||
)
|
||||
|
||||
|
||||
class RoborockSceneEntity(RoborockEntity, SceneEntity):
|
||||
"""A class to define Roborock scene entities."""
|
||||
|
||||
entity_description: EntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: RoborockDataUpdateCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
) -> None:
|
||||
"""Create a scene entity."""
|
||||
super().__init__(
|
||||
f"{entity_description.key}_{coordinator.duid_slug}",
|
||||
coordinator.device_info,
|
||||
coordinator.api,
|
||||
)
|
||||
self._scene_id = int(entity_description.key)
|
||||
self._coordinator = coordinator
|
||||
self.entity_description = entity_description
|
||||
|
||||
async def async_activate(self, **kwargs: Any) -> None:
|
||||
"""Activate the scene."""
|
||||
await self._coordinator.execute_scene(self._scene_id)
|
||||
@@ -174,11 +174,12 @@ def process_status(
|
||||
list[Capability | str],
|
||||
disabled_capabilities_capability[Attribute.DISABLED_CAPABILITIES].value,
|
||||
)
|
||||
for capability in disabled_capabilities:
|
||||
# We still need to make sure the climate entity can work without this capability
|
||||
if (
|
||||
capability in main_component
|
||||
and capability != Capability.DEMAND_RESPONSE_LOAD_CONTROL
|
||||
):
|
||||
del main_component[capability]
|
||||
if disabled_capabilities is not None:
|
||||
for capability in disabled_capabilities:
|
||||
# We still need to make sure the climate entity can work without this capability
|
||||
if (
|
||||
capability in main_component
|
||||
and capability != Capability.DEMAND_RESPONSE_LOAD_CONTROL
|
||||
):
|
||||
del main_component[capability]
|
||||
return status
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/webostv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiowebostv"],
|
||||
"requirements": ["aiowebostv==0.7.2"],
|
||||
"requirements": ["aiowebostv==0.7.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:lge-com:service:webos-second-screen:1"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
|
||||
import aiohttp
|
||||
@@ -18,7 +19,13 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
)
|
||||
|
||||
from .const import API_URL, LOGGER
|
||||
from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
HeatPumpInfo,
|
||||
WeheatConfigEntry,
|
||||
WeheatData,
|
||||
WeheatDataUpdateCoordinator,
|
||||
WeheatEnergyUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
@@ -52,14 +59,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: WeheatConfigEntry) -> bo
|
||||
except UnauthorizedException as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
||||
nr_of_pumps = len(discovered_heat_pumps)
|
||||
|
||||
for pump_info in discovered_heat_pumps:
|
||||
LOGGER.debug("Adding %s", pump_info)
|
||||
# for each pump, add a coordinator
|
||||
new_coordinator = WeheatDataUpdateCoordinator(hass, entry, session, pump_info)
|
||||
# for each pump, add the coordinators
|
||||
|
||||
await new_coordinator.async_config_entry_first_refresh()
|
||||
new_heat_pump = HeatPumpInfo(pump_info)
|
||||
new_data_coordinator = WeheatDataUpdateCoordinator(
|
||||
hass, entry, session, pump_info, nr_of_pumps
|
||||
)
|
||||
new_energy_coordinator = WeheatEnergyUpdateCoordinator(
|
||||
hass, entry, session, pump_info
|
||||
)
|
||||
|
||||
entry.runtime_data.append(new_coordinator)
|
||||
entry.runtime_data.append(
|
||||
WeheatData(
|
||||
heat_pump_info=new_heat_pump,
|
||||
data_coordinator=new_data_coordinator,
|
||||
energy_coordinator=new_energy_coordinator,
|
||||
)
|
||||
)
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
data.data_coordinator.async_config_entry_first_refresh()
|
||||
for data in entry.runtime_data
|
||||
],
|
||||
*[
|
||||
data.energy_coordinator.async_config_entry_first_refresh()
|
||||
for data in entry.runtime_data
|
||||
],
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator
|
||||
from .coordinator import HeatPumpInfo, WeheatConfigEntry, WeheatDataUpdateCoordinator
|
||||
from .entity import WeheatEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
@@ -68,10 +68,14 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the sensors for weheat heat pump."""
|
||||
entities = [
|
||||
WeheatHeatPumpBinarySensor(coordinator, entity_description)
|
||||
WeheatHeatPumpBinarySensor(
|
||||
weheatdata.heat_pump_info,
|
||||
weheatdata.data_coordinator,
|
||||
entity_description,
|
||||
)
|
||||
for weheatdata in entry.runtime_data
|
||||
for entity_description in BINARY_SENSORS
|
||||
for coordinator in entry.runtime_data
|
||||
if entity_description.value_fn(coordinator.data) is not None
|
||||
if entity_description.value_fn(weheatdata.data_coordinator.data) is not None
|
||||
]
|
||||
|
||||
async_add_entities(entities)
|
||||
@@ -80,20 +84,21 @@ async def async_setup_entry(
|
||||
class WeheatHeatPumpBinarySensor(WeheatEntity, BinarySensorEntity):
|
||||
"""Defines a Weheat heat pump binary sensor."""
|
||||
|
||||
heat_pump_info: HeatPumpInfo
|
||||
coordinator: WeheatDataUpdateCoordinator
|
||||
entity_description: WeHeatBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
heat_pump_info: HeatPumpInfo,
|
||||
coordinator: WeheatDataUpdateCoordinator,
|
||||
entity_description: WeHeatBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Pass coordinator to CoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
super().__init__(heat_pump_info, coordinator)
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_unique_id = f"{coordinator.heatpump_id}_{entity_description.key}"
|
||||
self._attr_unique_id = f"{heat_pump_info.heatpump_id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
||||
@@ -17,7 +17,8 @@ API_URL = "https://api.weheat.nl"
|
||||
OAUTH2_SCOPES = ["openid", "offline_access"]
|
||||
|
||||
|
||||
UPDATE_INTERVAL = 30
|
||||
LOG_UPDATE_INTERVAL = 120
|
||||
ENERGY_UPDATE_INTERVAL = 1800
|
||||
|
||||
LOGGER: Logger = getLogger(__package__)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Define a custom coordinator for the Weheat heatpump integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
from weheat.abstractions.discovery import HeatPumpDiscovery
|
||||
@@ -10,6 +11,7 @@ from weheat.exceptions import (
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
ServiceException,
|
||||
TooManyRequestsException,
|
||||
UnauthorizedException,
|
||||
)
|
||||
|
||||
@@ -21,7 +23,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import API_URL, DOMAIN, LOGGER, UPDATE_INTERVAL
|
||||
from .const import API_URL, DOMAIN, ENERGY_UPDATE_INTERVAL, LOG_UPDATE_INTERVAL, LOGGER
|
||||
|
||||
type WeheatConfigEntry = ConfigEntry[list[WeheatData]]
|
||||
|
||||
EXCEPTIONS = (
|
||||
ServiceException,
|
||||
@@ -29,9 +33,43 @@ EXCEPTIONS = (
|
||||
ForbiddenException,
|
||||
BadRequestException,
|
||||
ApiException,
|
||||
TooManyRequestsException,
|
||||
)
|
||||
|
||||
type WeheatConfigEntry = ConfigEntry[list[WeheatDataUpdateCoordinator]]
|
||||
|
||||
class HeatPumpInfo(HeatPumpDiscovery.HeatPumpInfo):
|
||||
"""Heat pump info with additional properties."""
|
||||
|
||||
def __init__(self, pump_info: HeatPumpDiscovery.HeatPumpInfo) -> None:
|
||||
"""Initialize the HeatPump object with the provided pump information.
|
||||
|
||||
Args:
|
||||
pump_info (HeatPumpDiscovery.HeatPumpInfo): An object containing the heat pump's discovery information, including:
|
||||
- uuid (str): Unique identifier for the heat pump.
|
||||
- uuid (str): Unique identifier for the heat pump.
|
||||
- device_name (str): Name of the heat pump device.
|
||||
- model (str): Model of the heat pump.
|
||||
- sn (str): Serial number of the heat pump.
|
||||
- has_dhw (bool): Indicates if the heat pump has domestic hot water functionality.
|
||||
|
||||
"""
|
||||
super().__init__(
|
||||
pump_info.uuid,
|
||||
pump_info.device_name,
|
||||
pump_info.model,
|
||||
pump_info.sn,
|
||||
pump_info.has_dhw,
|
||||
)
|
||||
|
||||
@property
|
||||
def readable_name(self) -> str | None:
|
||||
"""Return the readable name of the heat pump."""
|
||||
return self.device_name if self.device_name else self.model
|
||||
|
||||
@property
|
||||
def heatpump_id(self) -> str:
|
||||
"""Return the heat pump id."""
|
||||
return self.uuid
|
||||
|
||||
|
||||
class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]):
|
||||
@@ -45,45 +83,28 @@ class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]):
|
||||
config_entry: WeheatConfigEntry,
|
||||
session: OAuth2Session,
|
||||
heat_pump: HeatPumpDiscovery.HeatPumpInfo,
|
||||
nr_of_heat_pumps: int,
|
||||
) -> None:
|
||||
"""Initialize the data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=LOGGER,
|
||||
config_entry=config_entry,
|
||||
logger=LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
update_interval=timedelta(seconds=LOG_UPDATE_INTERVAL * nr_of_heat_pumps),
|
||||
)
|
||||
self.heat_pump_info = heat_pump
|
||||
self._heat_pump_data = HeatPump(
|
||||
API_URL, heat_pump.uuid, async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
self.session = session
|
||||
|
||||
@property
|
||||
def heatpump_id(self) -> str:
|
||||
"""Return the heat pump id."""
|
||||
return self.heat_pump_info.uuid
|
||||
|
||||
@property
|
||||
def readable_name(self) -> str | None:
|
||||
"""Return the readable name of the heat pump."""
|
||||
if self.heat_pump_info.name:
|
||||
return self.heat_pump_info.name
|
||||
return self.heat_pump_info.model
|
||||
|
||||
@property
|
||||
def model(self) -> str:
|
||||
"""Return the model of the heat pump."""
|
||||
return self.heat_pump_info.model
|
||||
|
||||
async def _async_update_data(self) -> HeatPump:
|
||||
"""Fetch data from the API."""
|
||||
await self.session.async_ensure_token_valid()
|
||||
|
||||
try:
|
||||
await self._heat_pump_data.async_get_status(
|
||||
await self._heat_pump_data.async_get_logs(
|
||||
self.session.token[CONF_ACCESS_TOKEN]
|
||||
)
|
||||
except UnauthorizedException as error:
|
||||
@@ -92,3 +113,54 @@ class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]):
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
return self._heat_pump_data
|
||||
|
||||
|
||||
class WeheatEnergyUpdateCoordinator(DataUpdateCoordinator[HeatPump]):
|
||||
"""A custom Energy coordinator for the Weheat heatpump integration."""
|
||||
|
||||
config_entry: WeheatConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: WeheatConfigEntry,
|
||||
session: OAuth2Session,
|
||||
heat_pump: HeatPumpDiscovery.HeatPumpInfo,
|
||||
) -> None:
|
||||
"""Initialize the data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry=config_entry,
|
||||
logger=LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=ENERGY_UPDATE_INTERVAL),
|
||||
)
|
||||
self._heat_pump_data = HeatPump(
|
||||
API_URL, heat_pump.uuid, async_get_clientsession(hass)
|
||||
)
|
||||
|
||||
self.session = session
|
||||
|
||||
async def _async_update_data(self) -> HeatPump:
|
||||
"""Fetch data from the API."""
|
||||
await self.session.async_ensure_token_valid()
|
||||
|
||||
try:
|
||||
await self._heat_pump_data.async_get_energy(
|
||||
self.session.token[CONF_ACCESS_TOKEN]
|
||||
)
|
||||
except UnauthorizedException as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
except EXCEPTIONS as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
return self._heat_pump_data
|
||||
|
||||
|
||||
@dataclass
|
||||
class WeheatData:
|
||||
"""Data for the Weheat integration."""
|
||||
|
||||
heat_pump_info: HeatPumpInfo
|
||||
data_coordinator: WeheatDataUpdateCoordinator
|
||||
energy_coordinator: WeheatEnergyUpdateCoordinator
|
||||
|
||||
@@ -3,25 +3,30 @@
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import HeatPumpInfo
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import WeheatDataUpdateCoordinator
|
||||
from .coordinator import WeheatDataUpdateCoordinator, WeheatEnergyUpdateCoordinator
|
||||
|
||||
|
||||
class WeheatEntity(CoordinatorEntity[WeheatDataUpdateCoordinator]):
|
||||
class WeheatEntity[
|
||||
_WeheatEntityT: WeheatDataUpdateCoordinator | WeheatEnergyUpdateCoordinator
|
||||
](CoordinatorEntity[_WeheatEntityT]):
|
||||
"""Defines a base Weheat entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WeheatDataUpdateCoordinator,
|
||||
heat_pump_info: HeatPumpInfo,
|
||||
coordinator: _WeheatEntityT,
|
||||
) -> None:
|
||||
"""Initialize the Weheat entity."""
|
||||
super().__init__(coordinator)
|
||||
self.heat_pump_info = heat_pump_info
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.heatpump_id)},
|
||||
name=coordinator.readable_name,
|
||||
identifiers={(DOMAIN, heat_pump_info.heatpump_id)},
|
||||
name=heat_pump_info.readable_name,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=coordinator.model,
|
||||
model=heat_pump_info.model,
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/weheat",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["weheat==2025.2.22"]
|
||||
"requirements": ["weheat==2025.2.26"]
|
||||
}
|
||||
|
||||
@@ -27,7 +27,12 @@ from .const import (
|
||||
DISPLAY_PRECISION_WATER_TEMP,
|
||||
DISPLAY_PRECISION_WATTS,
|
||||
)
|
||||
from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator
|
||||
from .coordinator import (
|
||||
HeatPumpInfo,
|
||||
WeheatConfigEntry,
|
||||
WeheatDataUpdateCoordinator,
|
||||
WeheatEnergyUpdateCoordinator,
|
||||
)
|
||||
from .entity import WeheatEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
@@ -142,22 +147,6 @@ SENSORS = [
|
||||
else None
|
||||
),
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="electricity_used",
|
||||
key="electricity_used",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_total,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="energy_output",
|
||||
key="energy_output",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_output,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="compressor_rpm",
|
||||
key="compressor_rpm",
|
||||
@@ -174,7 +163,6 @@ SENSORS = [
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
DHW_SENSORS = [
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="dhw_top_temperature",
|
||||
@@ -196,6 +184,25 @@ DHW_SENSORS = [
|
||||
),
|
||||
]
|
||||
|
||||
ENERGY_SENSORS = [
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="electricity_used",
|
||||
key="electricity_used",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_total,
|
||||
),
|
||||
WeHeatSensorEntityDescription(
|
||||
translation_key="energy_output",
|
||||
key="energy_output",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda status: status.energy_output,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -203,17 +210,39 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensors for weheat heat pump."""
|
||||
entities = [
|
||||
WeheatHeatPumpSensor(coordinator, entity_description)
|
||||
for entity_description in SENSORS
|
||||
for coordinator in entry.runtime_data
|
||||
]
|
||||
entities.extend(
|
||||
WeheatHeatPumpSensor(coordinator, entity_description)
|
||||
for entity_description in DHW_SENSORS
|
||||
for coordinator in entry.runtime_data
|
||||
if coordinator.heat_pump_info.has_dhw
|
||||
)
|
||||
|
||||
entities: list[WeheatHeatPumpSensor] = []
|
||||
for weheatdata in entry.runtime_data:
|
||||
entities.extend(
|
||||
WeheatHeatPumpSensor(
|
||||
weheatdata.heat_pump_info,
|
||||
weheatdata.data_coordinator,
|
||||
entity_description,
|
||||
)
|
||||
for entity_description in SENSORS
|
||||
if entity_description.value_fn(weheatdata.data_coordinator.data) is not None
|
||||
)
|
||||
if weheatdata.heat_pump_info.has_dhw:
|
||||
entities.extend(
|
||||
WeheatHeatPumpSensor(
|
||||
weheatdata.heat_pump_info,
|
||||
weheatdata.data_coordinator,
|
||||
entity_description,
|
||||
)
|
||||
for entity_description in DHW_SENSORS
|
||||
if entity_description.value_fn(weheatdata.data_coordinator.data)
|
||||
is not None
|
||||
)
|
||||
entities.extend(
|
||||
WeheatHeatPumpSensor(
|
||||
weheatdata.heat_pump_info,
|
||||
weheatdata.energy_coordinator,
|
||||
entity_description,
|
||||
)
|
||||
for entity_description in ENERGY_SENSORS
|
||||
if entity_description.value_fn(weheatdata.energy_coordinator.data)
|
||||
is not None
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -221,20 +250,21 @@ async def async_setup_entry(
|
||||
class WeheatHeatPumpSensor(WeheatEntity, SensorEntity):
|
||||
"""Defines a Weheat heat pump sensor."""
|
||||
|
||||
coordinator: WeheatDataUpdateCoordinator
|
||||
heat_pump_info: HeatPumpInfo
|
||||
coordinator: WeheatDataUpdateCoordinator | WeheatEnergyUpdateCoordinator
|
||||
entity_description: WeHeatSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: WeheatDataUpdateCoordinator,
|
||||
heat_pump_info: HeatPumpInfo,
|
||||
coordinator: WeheatDataUpdateCoordinator | WeheatEnergyUpdateCoordinator,
|
||||
entity_description: WeHeatSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Pass coordinator to CoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
super().__init__(heat_pump_info, coordinator)
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_unique_id = f"{coordinator.heatpump_id}_{entity_description.key}"
|
||||
self._attr_unique_id = f"{heat_pump_info.heatpump_id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "0b5"
|
||||
PATCH_VERSION: Final = "0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)
|
||||
|
||||
@@ -3640,7 +3640,7 @@
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"matter": {
|
||||
"name": "Matter (BETA)",
|
||||
"name": "Matter",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -37,8 +37,8 @@ habluetooth==3.24.1
|
||||
hass-nabucasa==0.92.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250228.0
|
||||
home-assistant-intents==2025.2.26
|
||||
home-assistant-frontend==20250305.0
|
||||
home-assistant-intents==2025.3.5
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
Jinja2==3.1.5
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.3.0b5"
|
||||
version = "2025.3.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
14
requirements_all.txt
generated
14
requirements_all.txt
generated
@@ -234,7 +234,7 @@ aioeafm==0.1.2
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2024.2.1
|
||||
aioecowitt==2025.3.1
|
||||
|
||||
# homeassistant.components.co2signal
|
||||
aioelectricitymaps==0.4.0
|
||||
@@ -425,7 +425,7 @@ aiowatttime==0.1.1
|
||||
aiowebdav2==0.3.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.2
|
||||
aiowebostv==0.7.3
|
||||
|
||||
# homeassistant.components.withings
|
||||
aiowithings==3.1.6
|
||||
@@ -1152,10 +1152,10 @@ hole==0.8.0
|
||||
holidays==0.68
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250228.0
|
||||
home-assistant-frontend==20250305.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.2.26
|
||||
home-assistant-intents==2025.3.5
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==1.1.7
|
||||
@@ -1483,7 +1483,7 @@ nettigo-air-monitor==4.0.0
|
||||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==2.1.1
|
||||
nexia==2.2.1
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.5.1
|
||||
@@ -1565,7 +1565,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.0.12
|
||||
onedrive-personal-sdk==0.0.13
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==3.2.5
|
||||
@@ -3058,7 +3058,7 @@ webio-api==0.1.11
|
||||
webmin-xmlrpc==0.0.2
|
||||
|
||||
# homeassistant.components.weheat
|
||||
weheat==2025.2.22
|
||||
weheat==2025.2.26
|
||||
|
||||
# homeassistant.components.whirlpool
|
||||
whirlpool-sixth-sense==0.18.12
|
||||
|
||||
14
requirements_test_all.txt
generated
14
requirements_test_all.txt
generated
@@ -222,7 +222,7 @@ aioeafm==0.1.2
|
||||
aioeagle==1.1.0
|
||||
|
||||
# homeassistant.components.ecowitt
|
||||
aioecowitt==2024.2.1
|
||||
aioecowitt==2025.3.1
|
||||
|
||||
# homeassistant.components.co2signal
|
||||
aioelectricitymaps==0.4.0
|
||||
@@ -407,7 +407,7 @@ aiowatttime==0.1.1
|
||||
aiowebdav2==0.3.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.2
|
||||
aiowebostv==0.7.3
|
||||
|
||||
# homeassistant.components.withings
|
||||
aiowithings==3.1.6
|
||||
@@ -981,10 +981,10 @@ hole==0.8.0
|
||||
holidays==0.68
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250228.0
|
||||
home-assistant-frontend==20250305.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.2.26
|
||||
home-assistant-intents==2025.3.5
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==1.1.7
|
||||
@@ -1246,7 +1246,7 @@ netmap==0.7.0.2
|
||||
nettigo-air-monitor==4.0.0
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==2.1.1
|
||||
nexia==2.2.1
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.5.1
|
||||
@@ -1313,7 +1313,7 @@ omnilogic==0.4.5
|
||||
ondilo==0.5.0
|
||||
|
||||
# homeassistant.components.onedrive
|
||||
onedrive-personal-sdk==0.0.12
|
||||
onedrive-personal-sdk==0.0.13
|
||||
|
||||
# homeassistant.components.onvif
|
||||
onvif-zeep-async==3.2.5
|
||||
@@ -2462,7 +2462,7 @@ webio-api==0.1.11
|
||||
webmin-xmlrpc==0.0.2
|
||||
|
||||
# homeassistant.components.weheat
|
||||
weheat==2025.2.22
|
||||
weheat==2025.2.26
|
||||
|
||||
# homeassistant.components.whirlpool
|
||||
whirlpool-sixth-sense==0.18.12
|
||||
|
||||
2
script/hassfest/docker/Dockerfile
generated
2
script/hassfest/docker/Dockerfile
generated
@@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.6.1,source=/uv,target=/bin/uv \
|
||||
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
|
||||
-r /usr/src/homeassistant/requirements.txt \
|
||||
stdlib-list==0.10.0 pipdeptree==2.25.0 tqdm==4.67.1 ruff==0.9.7 \
|
||||
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.2.26 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
|
||||
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.3.5 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
|
||||
|
||||
LABEL "name"="hassfest"
|
||||
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
|
||||
|
||||
@@ -180,7 +180,6 @@ EXCEPTIONS = {
|
||||
"PyMicroBot", # https://github.com/spycle/pyMicroBot/pull/3
|
||||
"PySwitchmate", # https://github.com/Danielhiversen/pySwitchmate/pull/16
|
||||
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
|
||||
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
|
||||
"chacha20poly1305", # LGPL
|
||||
"commentjson", # https://github.com/vaidik/commentjson/pull/55
|
||||
"crownstone-cloud", # https://github.com/crownstone/crownstone-lib-python-cloud/pull/5
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.DRY: 'dry'>,
|
||||
]),
|
||||
'max_temp': 30,
|
||||
'min_temp': 18,
|
||||
'max_temp': 86,
|
||||
'min_temp': 64,
|
||||
'preset_modes': list([
|
||||
'air_clean',
|
||||
]),
|
||||
@@ -28,7 +28,7 @@
|
||||
'on',
|
||||
'off',
|
||||
]),
|
||||
'target_temp_step': 1,
|
||||
'target_temp_step': 2,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
@@ -62,7 +62,7 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_humidity': 40,
|
||||
'current_temperature': 25,
|
||||
'current_temperature': 77,
|
||||
'fan_mode': 'mid',
|
||||
'fan_modes': list([
|
||||
'low',
|
||||
@@ -75,8 +75,8 @@
|
||||
<HVACMode.COOL: 'cool'>,
|
||||
<HVACMode.DRY: 'dry'>,
|
||||
]),
|
||||
'max_temp': 30,
|
||||
'min_temp': 18,
|
||||
'max_temp': 86,
|
||||
'min_temp': 64,
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
'air_clean',
|
||||
@@ -94,8 +94,8 @@
|
||||
]),
|
||||
'target_temp_high': None,
|
||||
'target_temp_low': None,
|
||||
'target_temp_step': 1,
|
||||
'temperature': 19,
|
||||
'target_temp_step': 2,
|
||||
'temperature': 66,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.test_air_conditioner',
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, patch
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import Platform, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
@@ -23,6 +23,7 @@ async def test_all_entities(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
hass.config.units.temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.CLIMATE]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ from .mock_data import (
|
||||
MULTI_MAP_LIST,
|
||||
NETWORK_INFO,
|
||||
PROP,
|
||||
SCENES,
|
||||
USER_DATA,
|
||||
USER_EMAIL,
|
||||
)
|
||||
@@ -68,24 +67,8 @@ class A01Mock(RoborockMqttClientA01):
|
||||
return {prot: self.protocol_responses[prot] for prot in dyad_data_protocols}
|
||||
|
||||
|
||||
@pytest.fixture(name="bypass_api_client_fixture")
|
||||
def bypass_api_client_fixture() -> None:
|
||||
"""Skip calls to the API client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
return_value=HOME_DATA,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_scenes",
|
||||
return_value=SCENES,
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(name="bypass_api_fixture")
|
||||
def bypass_api_fixture(bypass_api_client_fixture: Any) -> None:
|
||||
def bypass_api_fixture() -> None:
|
||||
"""Skip calls to the API."""
|
||||
with (
|
||||
patch("homeassistant.components.roborock.RoborockMqttClientV1.async_connect"),
|
||||
@@ -93,6 +76,10 @@ def bypass_api_fixture(bypass_api_client_fixture: Any) -> None:
|
||||
patch(
|
||||
"homeassistant.components.roborock.coordinator.RoborockMqttClientV1._send_command"
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_home_data_v2",
|
||||
return_value=HOME_DATA,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockMqttClientV1.get_networking",
|
||||
return_value=NETWORK_INFO,
|
||||
|
||||
@@ -9,7 +9,6 @@ from roborock.containers import (
|
||||
Consumable,
|
||||
DnDTimer,
|
||||
HomeData,
|
||||
HomeDataScene,
|
||||
MultiMapsList,
|
||||
NetworkInfo,
|
||||
S7Status,
|
||||
@@ -1151,19 +1150,3 @@ MAP_DATA = MapData(0, 0)
|
||||
MAP_DATA.image = ImageData(
|
||||
100, 10, 10, 10, 10, ImageConfig(), Image.new("RGB", (1, 1)), lambda p: p
|
||||
)
|
||||
|
||||
|
||||
SCENES = [
|
||||
HomeDataScene.from_dict(
|
||||
{
|
||||
"name": "sc1",
|
||||
"id": 12,
|
||||
},
|
||||
),
|
||||
HomeDataScene.from_dict(
|
||||
{
|
||||
"name": "sc2",
|
||||
"id": 24,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
"""Test Roborock Scene platform."""
|
||||
|
||||
from unittest.mock import ANY, patch
|
||||
|
||||
import pytest
|
||||
from roborock import RoborockException
|
||||
|
||||
from homeassistant.const import SERVICE_TURN_ON, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bypass_api_client_get_scenes_fixture(bypass_api_fixture) -> None:
|
||||
"""Fixture to raise when getting scenes."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.get_scenes",
|
||||
side_effect=RoborockException(),
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id"),
|
||||
[
|
||||
("scene.roborock_s7_maxv_sc1"),
|
||||
("scene.roborock_s7_maxv_sc2"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_get_scenes_failure(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_client_get_scenes_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test that if scene retrieval fails, no entity is being created."""
|
||||
# Ensure that the entity does not exist
|
||||
assert hass.states.get(entity_id) is None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to set platforms used in the test."""
|
||||
return [Platform.SCENE]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "scene_id"),
|
||||
[
|
||||
("scene.roborock_s7_maxv_sc1", 12),
|
||||
("scene.roborock_s7_maxv_sc2", 24),
|
||||
],
|
||||
)
|
||||
@pytest.mark.freeze_time("2023-10-30 08:50:00")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_execute_success(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
scene_id: int,
|
||||
) -> None:
|
||||
"""Test activating the scene entities."""
|
||||
with patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.execute_scene"
|
||||
) as mock_execute_scene:
|
||||
await hass.services.async_call(
|
||||
"scene",
|
||||
SERVICE_TURN_ON,
|
||||
blocking=True,
|
||||
target={"entity_id": entity_id},
|
||||
)
|
||||
mock_execute_scene.assert_called_once_with(ANY, scene_id)
|
||||
assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "scene_id"),
|
||||
[
|
||||
("scene.roborock_s7_maxv_sc1", 12),
|
||||
],
|
||||
)
|
||||
@pytest.mark.freeze_time("2023-10-30 08:50:00")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_execute_failure(
|
||||
hass: HomeAssistant,
|
||||
bypass_api_fixture,
|
||||
setup_entry: MockConfigEntry,
|
||||
entity_id: str,
|
||||
scene_id: int,
|
||||
) -> None:
|
||||
"""Test failure while activating the scene entity."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.roborock.RoborockApiClient.execute_scene",
|
||||
side_effect=RoborockException,
|
||||
) as mock_execute_scene,
|
||||
pytest.raises(HomeAssistantError, match="Error while calling execute_scene"),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"scene",
|
||||
SERVICE_TURN_ON,
|
||||
blocking=True,
|
||||
target={"entity_id": entity_id},
|
||||
)
|
||||
mock_execute_scene.assert_called_once_with(ANY, scene_id)
|
||||
assert hass.states.get(entity_id).state == "2023-10-30T08:50:00+00:00"
|
||||
Reference in New Issue
Block a user