forked from home-assistant/core
Compare commits
74 Commits
2025.3.0b3
...
2025.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e89948b5c | ||
|
|
9f95383201 | ||
|
|
7e452521c8 | ||
|
|
991de6f1d0 | ||
|
|
be32e3fe8f | ||
|
|
d6eb61e9ec | ||
|
|
e74fe69d65 | ||
|
|
208406123e | ||
|
|
8bcd135f3d | ||
|
|
e7ea0e435e | ||
|
|
b15b680cfe | ||
|
|
5e26d98bdf | ||
|
|
9f94ee280a | ||
|
|
efa98539fa | ||
|
|
113cd4bfcc | ||
|
|
ccbaf76e44 | ||
|
|
5d9d93d3a1 | ||
|
|
c2c5274aac | ||
|
|
89756394c9 | ||
|
|
352aa88e79 | ||
|
|
714962bd7a | ||
|
|
fb4c50b5dc | ||
|
|
b4794b2029 | ||
|
|
3a8c8accfe | ||
|
|
844adfc590 | ||
|
|
a279e23fb5 | ||
|
|
af9bbd0585 | ||
|
|
1304194f09 | ||
|
|
e909417a3f | ||
|
|
02706c116d | ||
|
|
3af6b5cb4c | ||
|
|
35c1bb1ec5 | ||
|
|
97cc3984c5 | ||
|
|
98e317dd55 | ||
|
|
ed088aa72f | ||
|
|
51162320cb | ||
|
|
b88eab8ba3 | ||
|
|
6c080ee650 | ||
|
|
8056b0df2b | ||
|
|
3f94b7a61c | ||
|
|
1484e46317 | ||
|
|
2812c8a993 | ||
|
|
5043e2ad10 | ||
|
|
2c2fd76270 | ||
|
|
7001f8daaf | ||
|
|
b41fc932c5 | ||
|
|
0872243297 | ||
|
|
bba889975a | ||
|
|
01e8ca6495 | ||
|
|
7d82375f81 | ||
|
|
47033e587b | ||
|
|
e73b08b269 | ||
|
|
a195a9107b | ||
|
|
185949cc18 | ||
|
|
c129f27c95 | ||
|
|
6a5a66e2f9 | ||
|
|
db63d9fcbf | ||
|
|
5b3d798eca | ||
|
|
a0dde2a7d6 | ||
|
|
1bdc33d52d | ||
|
|
f1d332da5a | ||
|
|
304c13261a | ||
|
|
c58cbfd6f4 | ||
|
|
b890d3e15a | ||
|
|
2c9b8b6835 | ||
|
|
73cc1f51ca | ||
|
|
dca77e8232 | ||
|
|
03cb177e7c | ||
|
|
ad04b53615 | ||
|
|
46bcb307f6 | ||
|
|
b816625028 | ||
|
|
0940fc7806 | ||
|
|
50aefc3653 | ||
|
|
c0dc83cbc0 |
1
homeassistant/components/apollo_automation/__init__.py
Normal file
1
homeassistant/components/apollo_automation/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Virtual integration: Apollo Automation."""
|
||||
6
homeassistant/components/apollo_automation/manifest.json
Normal file
6
homeassistant/components/apollo_automation/manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "apollo_automation",
|
||||
"name": "Apollo Automation",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "esphome"
|
||||
}
|
||||
@@ -153,6 +153,27 @@ def _has_min_duration(
|
||||
return validate
|
||||
|
||||
|
||||
def _has_positive_interval(
|
||||
start_key: str, end_key: str, duration_key: str
|
||||
) -> Callable[[dict[str, Any]], dict[str, Any]]:
|
||||
"""Verify that the time span between start and end is greater than zero."""
|
||||
|
||||
def validate(obj: dict[str, Any]) -> dict[str, Any]:
|
||||
if (duration := obj.get(duration_key)) is not None:
|
||||
if duration <= datetime.timedelta(seconds=0):
|
||||
raise vol.Invalid(f"Expected positive duration ({duration})")
|
||||
return obj
|
||||
|
||||
if (start := obj.get(start_key)) and (end := obj.get(end_key)):
|
||||
if start >= end:
|
||||
raise vol.Invalid(
|
||||
f"Expected end time to be after start time ({start}, {end})"
|
||||
)
|
||||
return obj
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
def _has_same_type(*keys: Any) -> Callable[[dict[str, Any]], dict[str, Any]]:
|
||||
"""Verify that all values are of the same type."""
|
||||
|
||||
@@ -281,6 +302,7 @@ SERVICE_GET_EVENTS_SCHEMA: Final = vol.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
_has_positive_interval(EVENT_START_DATETIME, EVENT_END_DATETIME, EVENT_DURATION),
|
||||
)
|
||||
|
||||
|
||||
@@ -870,6 +892,7 @@ async def async_get_events_service(
|
||||
end = start + service_call.data[EVENT_DURATION]
|
||||
else:
|
||||
end = service_call.data[EVENT_END_DATETIME]
|
||||
|
||||
calendar_event_list = await calendar.async_get_events(
|
||||
calendar.hass, dt_util.as_local(start), dt_util.as_local(end)
|
||||
)
|
||||
|
||||
@@ -49,7 +49,11 @@ def async_get_chat_log(
|
||||
raise RuntimeError(
|
||||
"Cannot attach chat log delta listener unless initial caller"
|
||||
)
|
||||
if user_input is not None:
|
||||
if user_input is not None and (
|
||||
(content := chat_log.content[-1]).role != "user"
|
||||
# MyPy doesn't understand that content is a UserContent here
|
||||
or content.content != user_input.text # type: ignore[union-attr]
|
||||
):
|
||||
chat_log.async_add_user_content(UserContent(content=user_input.text))
|
||||
|
||||
yield chat_log
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.13.5"]
|
||||
"requirements": ["sense-energy==0.13.6"]
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ DEFAULT_ALLOW_SERVICE_CALLS = True
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2025.2.1"
|
||||
STABLE_BLE_VERSION_STR = "2025.2.2"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
PROJECT_URLS = {
|
||||
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
|
||||
}
|
||||
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html"
|
||||
# ESPHome always uses .0 for the changelog URL
|
||||
STABLE_BLE_URL_VERSION = f"{STABLE_BLE_VERSION.major}.{STABLE_BLE_VERSION.minor}.0"
|
||||
DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_URL_VERSION}.html"
|
||||
|
||||
DATA_FFMPEG_PROXY = f"{DOMAIN}.ffmpeg_proxy"
|
||||
|
||||
@@ -11,6 +11,7 @@ from typing import Any
|
||||
import evohomeasync as ec1
|
||||
import evohomeasync2 as ec2
|
||||
from evohomeasync2.const import (
|
||||
SZ_DHW,
|
||||
SZ_GATEWAY_ID,
|
||||
SZ_GATEWAY_INFO,
|
||||
SZ_GATEWAYS,
|
||||
@@ -19,8 +20,9 @@ from evohomeasync2.const import (
|
||||
SZ_TEMPERATURE_CONTROL_SYSTEMS,
|
||||
SZ_TIME_ZONE,
|
||||
SZ_USE_DAYLIGHT_SAVE_SWITCHING,
|
||||
SZ_ZONES,
|
||||
)
|
||||
from evohomeasync2.schemas.typedefs import EvoLocStatusResponseT
|
||||
from evohomeasync2.schemas.typedefs import EvoLocStatusResponseT, EvoTcsConfigResponseT
|
||||
|
||||
from homeassistant.const import CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -113,17 +115,19 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
SZ_USE_DAYLIGHT_SAVE_SWITCHING
|
||||
],
|
||||
}
|
||||
tcs_info: EvoTcsConfigResponseT = self.tcs.config # type: ignore[assignment]
|
||||
tcs_info[SZ_ZONES] = [zone.config for zone in self.tcs.zones]
|
||||
if self.tcs.hotwater:
|
||||
tcs_info[SZ_DHW] = self.tcs.hotwater.config
|
||||
gwy_info = {
|
||||
SZ_GATEWAY_ID: self.loc.gateways[0].id,
|
||||
SZ_TEMPERATURE_CONTROL_SYSTEMS: [
|
||||
self.loc.gateways[0].systems[0].config
|
||||
],
|
||||
SZ_TEMPERATURE_CONTROL_SYSTEMS: [tcs_info],
|
||||
}
|
||||
config = {
|
||||
SZ_LOCATION_INFO: loc_info,
|
||||
SZ_GATEWAYS: [{SZ_GATEWAY_INFO: gwy_info}],
|
||||
}
|
||||
self.logger.debug("Config = %s", config)
|
||||
self.logger.debug("Config = %s", [config])
|
||||
|
||||
async def call_client_api(
|
||||
self,
|
||||
@@ -203,10 +207,18 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
async def _update_v2_schedules(self) -> None:
|
||||
for zone in self.tcs.zones:
|
||||
await zone.get_schedule()
|
||||
try:
|
||||
await zone.get_schedule()
|
||||
except ec2.InvalidScheduleError as err:
|
||||
self.logger.warning(
|
||||
"Zone '%s' has an invalid/missing schedule: %r", zone.name, err
|
||||
)
|
||||
|
||||
if dhw := self.tcs.hotwater:
|
||||
await dhw.get_schedule()
|
||||
try:
|
||||
await dhw.get_schedule()
|
||||
except ec2.InvalidScheduleError as err:
|
||||
self.logger.warning("DHW has an invalid/missing schedule: %r", err)
|
||||
|
||||
async def _async_update_data(self) -> EvoLocStatusResponseT: # type: ignore[override]
|
||||
"""Fetch the latest state of an entire TCC Location.
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import evohomeasync2 as evo
|
||||
from evohomeasync2.schemas.typedefs import DayOfWeekDhwT
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -102,7 +103,7 @@ class EvoChild(EvoEntity):
|
||||
|
||||
self._evo_tcs = evo_device.tcs
|
||||
|
||||
self._schedule: dict[str, Any] | None = None
|
||||
self._schedule: list[DayOfWeekDhwT] | None = None
|
||||
self._setpoints: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
@@ -123,6 +124,9 @@ class EvoChild(EvoEntity):
|
||||
Only Zones & DHW controllers (but not the TCS) can have schedules.
|
||||
"""
|
||||
|
||||
if not self._schedule:
|
||||
return self._setpoints
|
||||
|
||||
this_sp_dtm, this_sp_val = self._evo_device.this_switchpoint
|
||||
next_sp_dtm, next_sp_val = self._evo_device.next_switchpoint
|
||||
|
||||
@@ -152,10 +156,10 @@ class EvoChild(EvoEntity):
|
||||
self._evo_device,
|
||||
err,
|
||||
)
|
||||
self._schedule = {}
|
||||
self._schedule = []
|
||||
return
|
||||
else:
|
||||
self._schedule = schedule or {} # mypy hint
|
||||
self._schedule = schedule # type: ignore[assignment]
|
||||
|
||||
_LOGGER.debug("Schedule['%s'] = %s", self.name, schedule)
|
||||
|
||||
|
||||
@@ -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==20250306.0"]
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
prompt_parts = [call.data[CONF_PROMPT]]
|
||||
|
||||
config_entry: GoogleGenerativeAIConfigEntry = hass.config_entries.async_entries(
|
||||
DOMAIN
|
||||
)[0]
|
||||
config_entry: GoogleGenerativeAIConfigEntry = (
|
||||
hass.config_entries.async_loaded_entries(DOMAIN)[0]
|
||||
)
|
||||
|
||||
client = config_entry.runtime_data
|
||||
|
||||
|
||||
@@ -64,28 +64,18 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
SUPPORTED_SCHEMA_KEYS = {
|
||||
"min_items",
|
||||
"example",
|
||||
"property_ordering",
|
||||
"pattern",
|
||||
"minimum",
|
||||
"default",
|
||||
"any_of",
|
||||
"max_length",
|
||||
"title",
|
||||
"min_properties",
|
||||
"min_length",
|
||||
"max_items",
|
||||
"maximum",
|
||||
"nullable",
|
||||
"max_properties",
|
||||
# Gemini API does not support all of the OpenAPI schema
|
||||
# SoT: https://ai.google.dev/api/caching#Schema
|
||||
"type",
|
||||
"description",
|
||||
"enum",
|
||||
"format",
|
||||
"items",
|
||||
"description",
|
||||
"nullable",
|
||||
"enum",
|
||||
"max_items",
|
||||
"min_items",
|
||||
"properties",
|
||||
"required",
|
||||
"items",
|
||||
}
|
||||
|
||||
|
||||
@@ -109,9 +99,7 @@ def _format_schema(schema: dict[str, Any]) -> Schema:
|
||||
key = _camel_to_snake(key)
|
||||
if key not in SUPPORTED_SCHEMA_KEYS:
|
||||
continue
|
||||
if key == "any_of":
|
||||
val = [_format_schema(subschema) for subschema in val]
|
||||
elif key == "type":
|
||||
if key == "type":
|
||||
val = val.upper()
|
||||
elif key == "format":
|
||||
# Gemini API does not support all formats, see: https://ai.google.dev/api/caching#Schema
|
||||
|
||||
@@ -11,7 +11,6 @@ from hko import HKO, HKOError
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_CONDITION_FOG,
|
||||
ATTR_CONDITION_HAIL,
|
||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_POURING,
|
||||
@@ -145,7 +144,7 @@ class HKOUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Return the condition corresponding to the weather info."""
|
||||
info = info.lower()
|
||||
if WEATHER_INFO_RAIN in info:
|
||||
return ATTR_CONDITION_HAIL
|
||||
return ATTR_CONDITION_RAINY
|
||||
if WEATHER_INFO_SNOW in info and WEATHER_INFO_RAIN in info:
|
||||
return ATTR_CONDITION_SNOWY_RAINY
|
||||
if WEATHER_INFO_SNOW in info:
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.67", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.68", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -47,8 +47,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type HomeConnectConfigEntry = ConfigEntry[HomeConnectCoordinator]
|
||||
|
||||
EVENT_STREAM_RECONNECT_DELAY = 30
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeConnectApplianceData:
|
||||
@@ -100,6 +98,7 @@ class HomeConnectCoordinator(
|
||||
CALLBACK_TYPE, tuple[CALLBACK_TYPE, tuple[EventKey, ...]]
|
||||
] = {}
|
||||
self.device_registry = dr.async_get(self.hass)
|
||||
self.data = {}
|
||||
|
||||
@cached_property
|
||||
def context_listeners(self) -> dict[tuple[str, EventKey], list[CALLBACK_TYPE]]:
|
||||
@@ -157,10 +156,20 @@ class HomeConnectCoordinator(
|
||||
|
||||
async def _event_listener(self) -> None:
|
||||
"""Match event with listener for event type."""
|
||||
retry_time = 10
|
||||
while True:
|
||||
try:
|
||||
async for event_message in self.client.stream_all_events():
|
||||
retry_time = 10
|
||||
event_message_ha_id = event_message.ha_id
|
||||
if (
|
||||
event_message_ha_id in self.data
|
||||
and not self.data[event_message_ha_id].info.connected
|
||||
):
|
||||
self.data[event_message_ha_id].info.connected = True
|
||||
self._call_all_event_listeners_for_appliance(
|
||||
event_message_ha_id
|
||||
)
|
||||
match event_message.type:
|
||||
case EventType.STATUS:
|
||||
statuses = self.data[event_message_ha_id].status
|
||||
@@ -256,20 +265,18 @@ class HomeConnectCoordinator(
|
||||
except (EventStreamInterruptedError, HomeConnectRequestError) as error:
|
||||
_LOGGER.debug(
|
||||
"Non-breaking error (%s) while listening for events,"
|
||||
" continuing in 30 seconds",
|
||||
" continuing in %s seconds",
|
||||
type(error).__name__,
|
||||
retry_time,
|
||||
)
|
||||
await asyncio.sleep(EVENT_STREAM_RECONNECT_DELAY)
|
||||
await asyncio.sleep(retry_time)
|
||||
retry_time = min(retry_time * 2, 3600)
|
||||
except HomeConnectApiError as error:
|
||||
_LOGGER.error("Error while listening for events: %s", error)
|
||||
self.hass.config_entries.async_schedule_reload(
|
||||
self.config_entry.entry_id
|
||||
)
|
||||
break
|
||||
# if there was a non-breaking error, we continue listening
|
||||
# but we need to refresh the data to get the possible changes
|
||||
# that happened while the event stream was interrupted
|
||||
await self.async_refresh()
|
||||
|
||||
@callback
|
||||
def _call_event_listener(self, event_message: EventMessage) -> None:
|
||||
@@ -297,6 +304,8 @@ class HomeConnectCoordinator(
|
||||
translation_placeholders=get_dict_from_home_connect_error(error),
|
||||
) from error
|
||||
except HomeConnectError as error:
|
||||
for appliance_data in self.data.values():
|
||||
appliance_data.info.connected = False
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="fetch_api_error",
|
||||
@@ -305,7 +314,7 @@ class HomeConnectCoordinator(
|
||||
|
||||
return {
|
||||
appliance.ha_id: await self._get_appliance_data(
|
||||
appliance, self.data.get(appliance.ha_id) if self.data else None
|
||||
appliance, self.data.get(appliance.ha_id)
|
||||
)
|
||||
for appliance in appliances.homeappliances
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import cast
|
||||
from aiohomeconnect.model import EventKey, OptionKey
|
||||
from aiohomeconnect.model.error import ActiveProgramNotSetError, HomeConnectError
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -51,8 +52,10 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self.update_native_value()
|
||||
available = self._attr_available = self.appliance.info.connected
|
||||
self.async_write_ha_state()
|
||||
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, self.state)
|
||||
state = STATE_UNAVAILABLE if not available else self.state
|
||||
_LOGGER.debug("Updated %s, new state: %s", self.entity_id, state)
|
||||
|
||||
@property
|
||||
def bsh_key(self) -> str:
|
||||
@@ -61,10 +64,13 @@ class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
self.appliance.info.connected and self._attr_available and super().available
|
||||
)
|
||||
"""Return True if entity is available.
|
||||
|
||||
Do not use self.last_update_success for available state
|
||||
as event updates should take precedence over the coordinator
|
||||
refresh.
|
||||
"""
|
||||
return self._attr_available
|
||||
|
||||
|
||||
class HomeConnectOptionEntity(HomeConnectEntity):
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.15.1"],
|
||||
"requirements": ["aiohomeconnect==0.16.3"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -386,6 +386,13 @@ class HomeConnectProgramSensor(HomeConnectSensor):
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Update the program sensor's status."""
|
||||
self.program_running = (
|
||||
status := self.appliance.status.get(StatusKey.BSH_COMMON_OPERATION_STATE)
|
||||
) is not None and status.value in [
|
||||
BSH_OPERATION_STATE_RUN,
|
||||
BSH_OPERATION_STATE_PAUSE,
|
||||
BSH_OPERATION_STATE_FINISHED,
|
||||
]
|
||||
event = self.appliance.events.get(cast(EventKey, self.bsh_key))
|
||||
if event:
|
||||
self._update_native_value(event.value)
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"brightness": {
|
||||
"default": "mdi:brightness-5"
|
||||
},
|
||||
"brightness_instance": {
|
||||
"default": "mdi:brightness-5"
|
||||
},
|
||||
"link_quality": {
|
||||
"default": "mdi:signal"
|
||||
},
|
||||
|
||||
@@ -40,10 +40,22 @@ def get_window_value(attribute: HomeeAttribute) -> str | None:
|
||||
return vals.get(attribute.current_value)
|
||||
|
||||
|
||||
def get_brightness_device_class(
|
||||
attribute: HomeeAttribute, device_class: SensorDeviceClass | None
|
||||
) -> SensorDeviceClass | None:
|
||||
"""Return the device class for a brightness sensor."""
|
||||
if attribute.unit == "%":
|
||||
return None
|
||||
return device_class
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeeSensorEntityDescription(SensorEntityDescription):
|
||||
"""A class that describes Homee sensor entities."""
|
||||
|
||||
device_class_fn: Callable[
|
||||
[HomeeAttribute, SensorDeviceClass | None], SensorDeviceClass | None
|
||||
] = lambda attribute, device_class: device_class
|
||||
value_fn: Callable[[HomeeAttribute], str | float | None] = (
|
||||
lambda value: value.current_value
|
||||
)
|
||||
@@ -67,6 +79,7 @@ SENSOR_DESCRIPTIONS: dict[AttributeType, HomeeSensorEntityDescription] = {
|
||||
AttributeType.BRIGHTNESS: HomeeSensorEntityDescription(
|
||||
key="brightness",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
device_class_fn=get_brightness_device_class,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=(
|
||||
lambda attribute: attribute.current_value * 1000
|
||||
@@ -303,6 +316,9 @@ class HomeeSensor(HomeeEntity, SensorEntity):
|
||||
if attribute.instance > 0:
|
||||
self._attr_translation_key = f"{self._attr_translation_key}_instance"
|
||||
self._attr_translation_placeholders = {"instance": str(attribute.instance)}
|
||||
self._attr_device_class = description.device_class_fn(
|
||||
attribute, description.device_class
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | str | None:
|
||||
|
||||
@@ -111,6 +111,9 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"brightness": {
|
||||
"name": "Illuminance"
|
||||
},
|
||||
"brightness_instance": {
|
||||
"name": "Illuminance {instance}"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Pick Homematic IP access point",
|
||||
"description": "If you are about to register a **Homematic IP HCU1**, please press the button on top of the device before you continue.\n\nThe registration process must be completed within 5 minutes.",
|
||||
"data": {
|
||||
"hapid": "Access point ID (SGTIN)",
|
||||
"pin": "[%key:common::config_flow::data::pin%]",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -11,6 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import sensor
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASS_UNITS,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
ENTITY_ID_FORMAT,
|
||||
STATE_CLASSES_SCHEMA,
|
||||
@@ -107,6 +108,20 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
|
||||
)
|
||||
|
||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
|
||||
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
) is None:
|
||||
return config
|
||||
|
||||
if (
|
||||
device_class in DEVICE_CLASS_UNITS
|
||||
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
|
||||
):
|
||||
raise vol.Invalid(
|
||||
f"The unit of measurement `{unit_of_measurement}` is not valid "
|
||||
f"together with device class `{device_class}`"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
|
||||
from music_assistant_client import MusicAssistantClient
|
||||
from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion
|
||||
from music_assistant_models.enums import EventType
|
||||
from music_assistant_models.errors import MusicAssistantError
|
||||
from music_assistant_models.errors import ActionUnavailable, MusicAssistantError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
@@ -23,7 +23,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .actions import register_actions
|
||||
from .actions import get_music_assistant_client, register_actions
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -137,6 +137,18 @@ async def async_setup_entry(
|
||||
mass.subscribe(handle_player_removed, EventType.PLAYER_REMOVED)
|
||||
)
|
||||
|
||||
# check if any playerconfigs have been removed while we were disconnected
|
||||
all_player_configs = await mass.config.get_player_configs()
|
||||
player_ids = {player.player_id for player in all_player_configs}
|
||||
dev_reg = dr.async_get(hass)
|
||||
dev_entries = dr.async_entries_for_config_entry(dev_reg, entry.entry_id)
|
||||
for device in dev_entries:
|
||||
for identifier in device.identifiers:
|
||||
if identifier[0] == DOMAIN and identifier[1] not in player_ids:
|
||||
dev_reg.async_update_device(
|
||||
device.id, remove_config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -174,3 +186,31 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await mass_entry_data.mass.disconnect()
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove a config entry from a device."""
|
||||
player_id = next(
|
||||
(
|
||||
identifier[1]
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
),
|
||||
None,
|
||||
)
|
||||
if player_id is None:
|
||||
# this should not be possible at all, but guard it anyways
|
||||
return False
|
||||
mass = get_music_assistant_client(hass, config_entry.entry_id)
|
||||
if mass.players.get(player_id) is None:
|
||||
# player is already removed on the server, this is an orphaned device
|
||||
return True
|
||||
# try to remove the player from the server
|
||||
try:
|
||||
await mass.config.remove_player_config(player_id)
|
||||
except ActionUnavailable:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nest",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"requirements": ["google-nest-sdm==7.1.3"]
|
||||
"requirements": ["google-nest-sdm==7.1.4"]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"missing_credentials": "[%key:common::config_flow::abort::oauth2_missing_credentials%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nexia"],
|
||||
"requirements": ["nexia==2.0.9"]
|
||||
"requirements": ["nexia==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RoborockConfigEntry) ->
|
||||
translation_key="no_user_agreement",
|
||||
) from err
|
||||
except RoborockException as err:
|
||||
_LOGGER.debug("Failed to get Roborock home data: %s", err)
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to get Roborock home data",
|
||||
translation_domain=DOMAIN,
|
||||
@@ -82,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,
|
||||
)
|
||||
@@ -140,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,
|
||||
@@ -157,7 +151,6 @@ def build_setup_functions(
|
||||
device,
|
||||
product_info[device.product_id],
|
||||
home_data_rooms,
|
||||
api_client,
|
||||
)
|
||||
for device in device_map.values()
|
||||
]
|
||||
@@ -170,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)
|
||||
@@ -195,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(
|
||||
@@ -217,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.
|
||||
@@ -179,6 +166,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
# Get the rooms for that map id.
|
||||
await self.set_current_map_rooms()
|
||||
except RoborockException as ex:
|
||||
_LOGGER.debug("Failed to update data: %s", ex)
|
||||
raise UpdateFailed(ex) from ex
|
||||
return self.roborock_device_info.props
|
||||
|
||||
@@ -206,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."""
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
import io
|
||||
import logging
|
||||
|
||||
from roborock import RoborockCommand
|
||||
from vacuum_map_parser_base.config.color import ColorsPalette
|
||||
@@ -30,6 +31,8 @@ from .const import (
|
||||
from .coordinator import RoborockConfigEntry, RoborockDataUpdateCoordinator
|
||||
from .entity import RoborockCoordinatedEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -48,7 +51,11 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
def parse_image(map_bytes: bytes) -> bytes | None:
|
||||
parsed_map = parser.parse(map_bytes)
|
||||
try:
|
||||
parsed_map = parser.parse(map_bytes)
|
||||
except (IndexError, ValueError) as err:
|
||||
_LOGGER.debug("Exception when parsing map contents: %s", err)
|
||||
return None
|
||||
if parsed_map.image is None:
|
||||
return None
|
||||
img_byte_arr = io.BytesIO()
|
||||
@@ -150,6 +157,7 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
not isinstance(response[0], bytes)
|
||||
or (content := self.parser(response[0])) is None
|
||||
):
|
||||
_LOGGER.debug("Failed to parse map contents: %s", response[0])
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="map_failure",
|
||||
|
||||
@@ -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)
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sense",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sense_energy"],
|
||||
"requirements": ["sense-energy==0.13.5"]
|
||||
"requirements": ["sense-energy==0.13.6"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast
|
||||
@@ -11,6 +12,7 @@ from pysmartthings import (
|
||||
Attribute,
|
||||
Capability,
|
||||
Device,
|
||||
DeviceEvent,
|
||||
Scene,
|
||||
SmartThings,
|
||||
SmartThingsAuthenticationFailedError,
|
||||
@@ -28,7 +30,14 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .const import CONF_INSTALLED_APP_ID, CONF_LOCATION_ID, DOMAIN, MAIN, OLD_DATA
|
||||
from .const import (
|
||||
CONF_INSTALLED_APP_ID,
|
||||
CONF_LOCATION_ID,
|
||||
DOMAIN,
|
||||
EVENT_BUTTON,
|
||||
MAIN,
|
||||
OLD_DATA,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -114,6 +123,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmartThingsConfigEntry)
|
||||
scenes=scenes,
|
||||
)
|
||||
|
||||
def handle_button_press(event: DeviceEvent) -> None:
|
||||
"""Handle a button press."""
|
||||
if (
|
||||
event.capability is Capability.BUTTON
|
||||
and event.attribute is Attribute.BUTTON
|
||||
):
|
||||
hass.bus.async_fire(
|
||||
EVENT_BUTTON,
|
||||
{
|
||||
"component_id": event.component_id,
|
||||
"device_id": event.device_id,
|
||||
"location_id": event.location_id,
|
||||
"value": event.value,
|
||||
"name": entry.runtime_data.devices[event.device_id].device.label,
|
||||
"data": event.data,
|
||||
},
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
client.add_unspecified_device_event_listener(handle_button_press)
|
||||
)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
client.subscribe(
|
||||
@@ -160,25 +191,62 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
KEEP_CAPABILITY_QUIRK: dict[
|
||||
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
|
||||
] = {
|
||||
Capability.WASHER_OPERATING_STATE: (
|
||||
lambda status: status[Attribute.SUPPORTED_MACHINE_STATES].value is not None
|
||||
),
|
||||
Capability.DEMAND_RESPONSE_LOAD_CONTROL: lambda _: True,
|
||||
}
|
||||
|
||||
POWER_CONSUMPTION_FIELDS = {
|
||||
"energy",
|
||||
"power",
|
||||
"deltaEnergy",
|
||||
"powerEnergy",
|
||||
"energySaved",
|
||||
}
|
||||
|
||||
CAPABILITY_VALIDATION: dict[
|
||||
Capability | str, Callable[[dict[Attribute | str, Status]], bool]
|
||||
] = {
|
||||
Capability.POWER_CONSUMPTION_REPORT: (
|
||||
lambda status: (
|
||||
(power_consumption := status[Attribute.POWER_CONSUMPTION].value) is not None
|
||||
and all(
|
||||
field in cast(dict, power_consumption)
|
||||
for field in POWER_CONSUMPTION_FIELDS
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def process_status(
|
||||
status: dict[str, dict[Capability | str, dict[Attribute | str, Status]]],
|
||||
) -> dict[str, dict[Capability | str, dict[Attribute | str, Status]]]:
|
||||
"""Remove disabled capabilities from status."""
|
||||
if (main_component := status.get("main")) is None or (
|
||||
if (main_component := status.get(MAIN)) is None:
|
||||
return status
|
||||
if (
|
||||
disabled_capabilities_capability := main_component.get(
|
||||
Capability.CUSTOM_DISABLED_CAPABILITIES
|
||||
)
|
||||
) is None:
|
||||
return status
|
||||
disabled_capabilities = cast(
|
||||
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]
|
||||
) is not None:
|
||||
disabled_capabilities = cast(
|
||||
list[Capability | str],
|
||||
disabled_capabilities_capability[Attribute.DISABLED_CAPABILITIES].value,
|
||||
)
|
||||
if disabled_capabilities is not None:
|
||||
for capability in disabled_capabilities:
|
||||
if capability in main_component and (
|
||||
capability not in KEEP_CAPABILITY_QUIRK
|
||||
or not KEEP_CAPABILITY_QUIRK[capability](main_component[capability])
|
||||
):
|
||||
del main_component[capability]
|
||||
for capability in list(main_component):
|
||||
if capability in CAPABILITY_VALIDATION:
|
||||
if not CAPABILITY_VALIDATION[capability](main_component[capability]):
|
||||
del main_component[capability]
|
||||
return status
|
||||
|
||||
@@ -161,9 +161,7 @@ class SmartThingsThermostat(SmartThingsEntity, ClimateEntity):
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
)
|
||||
if self.get_attribute_value(
|
||||
Capability.THERMOSTAT_FAN_MODE, Attribute.THERMOSTAT_FAN_MODE
|
||||
):
|
||||
if self.supports_capability(Capability.THERMOSTAT_FAN_MODE):
|
||||
flags |= ClimateEntityFeature.FAN_MODE
|
||||
return flags
|
||||
|
||||
@@ -445,12 +443,15 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return device specific state attributes.
|
||||
|
||||
Include attributes from the Demand Response Load Control (drlc)
|
||||
and Power Consumption capabilities.
|
||||
"""
|
||||
if not self.supports_capability(Capability.DEMAND_RESPONSE_LOAD_CONTROL):
|
||||
return None
|
||||
|
||||
drlc_status = self.get_attribute_value(
|
||||
Capability.DEMAND_RESPONSE_LOAD_CONTROL,
|
||||
Attribute.DEMAND_RESPONSE_LOAD_CONTROL_STATUS,
|
||||
@@ -560,5 +561,6 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
Capability.AIR_CONDITIONER_MODE, Attribute.SUPPORTED_AC_MODES
|
||||
)
|
||||
if (state := AC_MODE_TO_STATE.get(mode)) is not None
|
||||
if state not in modes
|
||||
)
|
||||
return modes
|
||||
|
||||
@@ -32,9 +32,20 @@ class SmartThingsConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
return {"scope": " ".join(REQUESTED_SCOPES)}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Check we have the cloud integration set up."""
|
||||
if "cloud" not in self.hass.config.components:
|
||||
return self.async_abort(
|
||||
reason="cloud_not_enabled",
|
||||
description_placeholders={"default_config": "default_config"},
|
||||
)
|
||||
return await super().async_step_user(user_input)
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for SmartThings."""
|
||||
if data[CONF_TOKEN]["scope"].split() != SCOPES:
|
||||
if not set(data[CONF_TOKEN]["scope"].split()) >= set(SCOPES):
|
||||
return self.async_abort(reason="missing_scopes")
|
||||
client = SmartThings(session=async_get_clientsession(self.hass))
|
||||
client.authenticate(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||
|
||||
@@ -32,3 +32,5 @@ CONF_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
MAIN = "main"
|
||||
OLD_DATA = "old_data"
|
||||
|
||||
EVENT_BUTTON = "smartthings.button"
|
||||
|
||||
@@ -17,6 +17,15 @@ from .const import DOMAIN
|
||||
EVENT_WAIT_TIME = 5
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: SmartThingsConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
client = entry.runtime_data.client
|
||||
return await client.get_raw_devices()
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: SmartThingsConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
@@ -26,7 +35,8 @@ async def async_get_device_diagnostics(
|
||||
identifier for identifier in device.identifiers if identifier[0] == DOMAIN
|
||||
)[1]
|
||||
|
||||
device_status = await client.get_device_status(device_id)
|
||||
device_status = await client.get_raw_device_status(device_id)
|
||||
device_info = await client.get_raw_device(device_id)
|
||||
|
||||
events: list[DeviceEvent] = []
|
||||
|
||||
@@ -39,11 +49,8 @@ async def async_get_device_diagnostics(
|
||||
|
||||
listener()
|
||||
|
||||
status: dict[str, Any] = {}
|
||||
for component, capabilities in device_status.items():
|
||||
status[component] = {}
|
||||
for capability, attributes in capabilities.items():
|
||||
status[component][capability] = {}
|
||||
for attribute, value in attributes.items():
|
||||
status[component][capability][attribute] = asdict(value)
|
||||
return {"events": [asdict(event) for event in events], "status": status}
|
||||
return {
|
||||
"events": [asdict(event) for event in events],
|
||||
"status": device_status,
|
||||
"info": device_info,
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ class SmartThingsEntity(Entity):
|
||||
self._attr_device_info.update(
|
||||
{
|
||||
"manufacturer": ocf.manufacturer_name,
|
||||
"model": ocf.model_number.split("|")[0],
|
||||
"model": (
|
||||
(ocf.model_number.split("|")[0]) if ocf.model_number else None
|
||||
),
|
||||
"hw_version": ocf.hardware_version,
|
||||
"sw_version": ocf.firmware_version,
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if fan is on."""
|
||||
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH)
|
||||
return self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on"
|
||||
|
||||
@property
|
||||
def percentage(self) -> int | None:
|
||||
@@ -132,6 +132,8 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
|
||||
|
||||
Requires FanEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
if not self.supports_capability(Capability.AIR_CONDITIONER_FAN_MODE):
|
||||
return None
|
||||
return self.get_attribute_value(
|
||||
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.FAN_MODE
|
||||
)
|
||||
@@ -142,6 +144,8 @@ class SmartThingsFan(SmartThingsEntity, FanEntity):
|
||||
|
||||
Requires FanEntityFeature.PRESET_MODE.
|
||||
"""
|
||||
if not self.supports_capability(Capability.AIR_CONDITIONER_FAN_MODE):
|
||||
return None
|
||||
return self.get_attribute_value(
|
||||
Capability.AIR_CONDITIONER_FAN_MODE, Attribute.SUPPORTED_AC_FAN_MODES
|
||||
)
|
||||
|
||||
@@ -29,5 +29,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smartthings",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"requirements": ["pysmartthings==2.4.1"]
|
||||
"requirements": ["pysmartthings==2.7.0"]
|
||||
}
|
||||
|
||||
@@ -130,7 +130,6 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
|
||||
unique_id_separator: str = "."
|
||||
capability_ignore_list: list[set[Capability]] | None = None
|
||||
options_attribute: Attribute | None = None
|
||||
except_if_state_none: bool = False
|
||||
|
||||
|
||||
CAPABILITY_TO_SENSORS: dict[
|
||||
@@ -581,7 +580,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
value_fn=lambda value: value["energy"] / 1000,
|
||||
suggested_display_precision=2,
|
||||
except_if_state_none=True,
|
||||
),
|
||||
SmartThingsSensorEntityDescription(
|
||||
key="power_meter",
|
||||
@@ -591,7 +589,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
value_fn=lambda value: value["power"],
|
||||
extra_state_attributes_fn=power_attributes,
|
||||
suggested_display_precision=2,
|
||||
except_if_state_none=True,
|
||||
),
|
||||
SmartThingsSensorEntityDescription(
|
||||
key="deltaEnergy_meter",
|
||||
@@ -601,7 +598,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
value_fn=lambda value: value["deltaEnergy"] / 1000,
|
||||
suggested_display_precision=2,
|
||||
except_if_state_none=True,
|
||||
),
|
||||
SmartThingsSensorEntityDescription(
|
||||
key="powerEnergy_meter",
|
||||
@@ -611,7 +607,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
value_fn=lambda value: value["powerEnergy"] / 1000,
|
||||
suggested_display_precision=2,
|
||||
except_if_state_none=True,
|
||||
),
|
||||
SmartThingsSensorEntityDescription(
|
||||
key="energySaved_meter",
|
||||
@@ -621,7 +616,6 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
value_fn=lambda value: value["energySaved"] / 1000,
|
||||
suggested_display_precision=2,
|
||||
except_if_state_none=True,
|
||||
),
|
||||
]
|
||||
},
|
||||
@@ -951,6 +945,7 @@ UNITS = {
|
||||
"F": UnitOfTemperature.FAHRENHEIT,
|
||||
"lux": LIGHT_LUX,
|
||||
"mG": None,
|
||||
"μg/m^3": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
}
|
||||
|
||||
|
||||
@@ -975,10 +970,6 @@ async def async_setup_entry(
|
||||
for capability_list in description.capability_ignore_list
|
||||
)
|
||||
)
|
||||
and (
|
||||
not description.except_if_state_none
|
||||
or device.status[MAIN][capability][attribute].value is not None
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reauth_account_mismatch": "Authenticated account does not match the account to be reauthenticated. Please log in with the correct account and pick the right location.",
|
||||
"reauth_location_mismatch": "Authenticated location does not match the location to be reauthenticated. Please log in with the correct account and pick the right location.",
|
||||
"missing_scopes": "Authentication failed. Please make sure you have granted all required permissions."
|
||||
"missing_scopes": "Authentication failed. Please make sure you have granted all required permissions.",
|
||||
"cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["snoo"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-snoo==0.6.0"]
|
||||
"requirements": ["python-snoo==0.6.1"]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=STATUS_SENSOR_INFO_TOTAL_GENRES,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.7.0"],
|
||||
"requirements": ["py-synologydsm-api==2.7.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Synology",
|
||||
|
||||
@@ -466,6 +466,7 @@ async def async_setup_entry(
|
||||
for energysite in entry.runtime_data.energysites
|
||||
for description in ENERGY_LIVE_DESCRIPTIONS
|
||||
if description.key in energysite.live_coordinator.data
|
||||
or description.key == "percentage_charged"
|
||||
),
|
||||
( # Add energy site history
|
||||
TeslaFleetEnergyHistorySensorEntity(energysite, description)
|
||||
|
||||
@@ -68,7 +68,7 @@ class TeslemetryVehicleSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
polling: bool = False
|
||||
polling_value_fn: Callable[[StateType], StateType] = lambda x: x
|
||||
polling_available_fn: Callable[[StateType], bool] = lambda x: x is not None
|
||||
nullable: bool = False
|
||||
streaming_key: Signal | None = None
|
||||
streaming_value_fn: Callable[[str | int | float], StateType] = lambda x: x
|
||||
streaming_firmware: str = "2024.26"
|
||||
@@ -210,7 +210,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_shift_state",
|
||||
polling=True,
|
||||
polling_available_fn=lambda x: True,
|
||||
nullable=True,
|
||||
polling_value_fn=lambda x: SHIFT_STATES.get(str(x), "p"),
|
||||
streaming_key=Signal.GEAR,
|
||||
streaming_value_fn=lambda x: str(ShiftState.get(x, "P")).lower(),
|
||||
@@ -622,10 +622,10 @@ class TeslemetryStreamSensorEntity(TeslemetryVehicleStreamEntity, RestoreSensor)
|
||||
|
||||
def _async_value_from_stream(self, value) -> None:
|
||||
"""Update the value of the entity."""
|
||||
if value is None:
|
||||
self._attr_native_value = None
|
||||
else:
|
||||
if self.entity_description.nullable or value is not None:
|
||||
self._attr_native_value = self.entity_description.streaming_value_fn(value)
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
|
||||
|
||||
class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
|
||||
@@ -644,7 +644,7 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity):
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the attributes of the sensor."""
|
||||
if self.entity_description.polling_available_fn(self._value):
|
||||
if self.entity_description.nullable or self._value is not None:
|
||||
self._attr_available = True
|
||||
self._attr_native_value = self.entity_description.polling_value_fn(
|
||||
self._value
|
||||
|
||||
@@ -148,7 +148,7 @@ DESCRIPTIONS: tuple[TessieSensorEntityDescription, ...] = (
|
||||
key="drive_state_shift_state",
|
||||
options=["p", "d", "r", "n"],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda x: x.lower() if isinstance(x, str) else x,
|
||||
value_fn=lambda x: x.lower() if isinstance(x, str) else "p",
|
||||
),
|
||||
TessieSensorEntityDescription(
|
||||
key="vehicle_state_odometer",
|
||||
@@ -397,6 +397,7 @@ async def async_setup_entry(
|
||||
for energysite in entry.runtime_data.energysites
|
||||
for description in ENERGY_LIVE_DESCRIPTIONS
|
||||
if description.key in energysite.live_coordinator.data
|
||||
or description.key == "percentage_charged"
|
||||
),
|
||||
( # Add wall connectors
|
||||
TessieWallConnectorSensorEntity(energysite, din, description)
|
||||
@@ -449,7 +450,6 @@ class TessieEnergyLiveSensorEntity(TessieEnergyEntity, SensorEntity):
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the attributes of the sensor."""
|
||||
self._attr_available = self._value is not None
|
||||
self._attr_native_value = self.entity_description.value_fn(self._value)
|
||||
|
||||
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/thermobeacon",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["thermobeacon-ble==0.8.0"]
|
||||
"requirements": ["thermobeacon-ble==0.8.1"]
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ def remove_stale_devices(
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
all_device_ids = {device.id for device in devices}
|
||||
all_device_ids = {str(device.id) for device in devices}
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
@@ -176,7 +176,7 @@ def remove_stale_devices(
|
||||
gateway_id = _id
|
||||
break
|
||||
|
||||
device_id = _id
|
||||
device_id = _id.replace(f"{config_entry.data[CONF_GATEWAY_ID]}-", "")
|
||||
break
|
||||
|
||||
if gateway_id is not None:
|
||||
@@ -190,3 +190,93 @@ def remove_stale_devices(
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
LOGGER.debug(
|
||||
"Migrating Tradfri configuration from version %s.%s",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
if config_entry.version > 1:
|
||||
# This means the user has downgraded from a future version
|
||||
return False
|
||||
|
||||
if config_entry.version == 1:
|
||||
# Migrate to version 2
|
||||
migrate_config_entry_and_identifiers(hass, config_entry)
|
||||
|
||||
hass.config_entries.async_update_entry(config_entry, version=2)
|
||||
|
||||
LOGGER.debug(
|
||||
"Migration to Tradfri configuration version %s.%s successful",
|
||||
config_entry.version,
|
||||
config_entry.minor_version,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def migrate_config_entry_and_identifiers(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Migrate old non-unique identifiers to new unique identifiers."""
|
||||
|
||||
related_device_flag: bool
|
||||
device_id: str
|
||||
|
||||
device_reg = dr.async_get(hass)
|
||||
# Get all devices associated to contextual gateway config_entry
|
||||
# and loop through list of devices.
|
||||
for device in dr.async_entries_for_config_entry(device_reg, config_entry.entry_id):
|
||||
related_device_flag = False
|
||||
for identifier in device.identifiers:
|
||||
if identifier[0] != DOMAIN:
|
||||
continue
|
||||
|
||||
related_device_flag = True
|
||||
|
||||
_id = identifier[1]
|
||||
|
||||
# Identify gateway device.
|
||||
if _id == config_entry.data[CONF_GATEWAY_ID]:
|
||||
# Using this to avoid updating gateway's own device registry entry
|
||||
related_device_flag = False
|
||||
break
|
||||
|
||||
device_id = str(_id)
|
||||
break
|
||||
|
||||
# Check that device is related to tradfri domain (and is not the gateway itself)
|
||||
if not related_device_flag:
|
||||
continue
|
||||
|
||||
# Loop through list of config_entry_ids for device
|
||||
config_entry_ids = device.config_entries
|
||||
for config_entry_id in config_entry_ids:
|
||||
# Check that the config entry in list is not the device's primary config entry
|
||||
if config_entry_id == device.primary_config_entry:
|
||||
continue
|
||||
|
||||
# Check that the 'other' config entry is also a tradfri config entry
|
||||
other_entry = hass.config_entries.async_get_entry(config_entry_id)
|
||||
|
||||
if other_entry is None or other_entry.domain != DOMAIN:
|
||||
continue
|
||||
|
||||
# Remove non-primary 'tradfri' config entry from device's config_entry_ids
|
||||
device_reg.async_update_device(
|
||||
device.id, remove_config_entry_id=config_entry_id
|
||||
)
|
||||
|
||||
if config_entry.data[CONF_GATEWAY_ID] in device_id:
|
||||
continue
|
||||
|
||||
device_reg.async_update_device(
|
||||
device.id,
|
||||
new_identifiers={
|
||||
(DOMAIN, f"{config_entry.data[CONF_GATEWAY_ID]}-{device_id}")
|
||||
},
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ class AuthError(Exception):
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
|
||||
@@ -58,7 +58,7 @@ class TradfriBaseEntity(CoordinatorEntity[TradfriDeviceDataUpdateCoordinator]):
|
||||
|
||||
info = self._device.device_info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device_id)},
|
||||
identifiers={(DOMAIN, f"{gateway_id}-{self._device_id}")},
|
||||
manufacturer=info.manufacturer,
|
||||
model=info.model_number,
|
||||
name=self._device.name,
|
||||
|
||||
@@ -196,7 +196,10 @@ class ViCareFan(ViCareEntity, FanEntity):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the entity is on."""
|
||||
if self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY):
|
||||
if (
|
||||
self._attr_supported_features & FanEntityFeature.TURN_OFF
|
||||
and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY)
|
||||
):
|
||||
return False
|
||||
|
||||
return self.percentage is not None and self.percentage > 0
|
||||
@@ -209,7 +212,10 @@ class ViCareFan(ViCareEntity, FanEntity):
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon to use in the frontend."""
|
||||
if self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY):
|
||||
if (
|
||||
self._attr_supported_features & FanEntityFeature.TURN_OFF
|
||||
and self._api.getVentilationQuickmode(VentilationQuickmode.STANDBY)
|
||||
):
|
||||
return "mdi:fan-off"
|
||||
if hasattr(self, "_attr_preset_mode"):
|
||||
if self._attr_preset_mode == VentilationMode.VENTILATION:
|
||||
|
||||
@@ -171,6 +171,7 @@ class WebDavBackupAgent(BackupAgent):
|
||||
await open_stream(),
|
||||
f"{self._backup_path}/{filename_tar}",
|
||||
timeout=BACKUP_TIMEOUT,
|
||||
content_length=backup.size,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiowebdav2"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiowebdav2==0.3.1"]
|
||||
"requirements": ["aiowebdav2==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/webostv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiowebostv"],
|
||||
"requirements": ["aiowebostv==0.7.1"],
|
||||
"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:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.67"]
|
||||
"requirements": ["holidays==0.68"]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "0b3"
|
||||
PATCH_VERSION: Final = "1"
|
||||
__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)
|
||||
|
||||
@@ -345,6 +345,11 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"apollo_automation": {
|
||||
"name": "Apollo Automation",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "esphome"
|
||||
},
|
||||
"appalachianpower": {
|
||||
"name": "Appalachian Power",
|
||||
"integration_type": "virtual",
|
||||
@@ -3635,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==20250306.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.0b3"
|
||||
version = "2025.3.1"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
32
requirements_all.txt
generated
32
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
|
||||
@@ -264,7 +264,7 @@ aioharmony==0.4.1
|
||||
aiohasupervisor==0.3.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.15.1
|
||||
aiohomeconnect==0.16.3
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.8
|
||||
@@ -422,10 +422,10 @@ aiowaqi==3.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webdav
|
||||
aiowebdav2==0.3.1
|
||||
aiowebdav2==0.4.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.1
|
||||
aiowebostv==0.7.3
|
||||
|
||||
# homeassistant.components.withings
|
||||
aiowithings==3.1.6
|
||||
@@ -1042,7 +1042,7 @@ google-cloud-texttospeech==2.17.2
|
||||
google-genai==1.1.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==7.1.3
|
||||
google-nest-sdm==7.1.4
|
||||
|
||||
# homeassistant.components.google_photos
|
||||
google-photos-library-api==0.12.1
|
||||
@@ -1149,13 +1149,13 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.67
|
||||
holidays==0.68
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250228.0
|
||||
home-assistant-frontend==20250306.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.0.9
|
||||
nexia==2.2.2
|
||||
|
||||
# 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
|
||||
@@ -1755,7 +1755,7 @@ py-schluter==0.1.7
|
||||
py-sucks==0.9.10
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.7.0
|
||||
py-synologydsm-api==2.7.1
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyAtome==0.1.1
|
||||
@@ -2310,7 +2310,7 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==2.4.1
|
||||
pysmartthings==2.7.0
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@@ -2467,7 +2467,7 @@ python-roborock==2.11.1
|
||||
python-smarttub==0.0.39
|
||||
|
||||
# homeassistant.components.snoo
|
||||
python-snoo==0.6.0
|
||||
python-snoo==0.6.1
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.16.2
|
||||
@@ -2694,7 +2694,7 @@ sendgrid==6.8.2
|
||||
|
||||
# homeassistant.components.emulated_kasa
|
||||
# homeassistant.components.sense
|
||||
sense-energy==0.13.5
|
||||
sense-energy==0.13.6
|
||||
|
||||
# homeassistant.components.sensirion_ble
|
||||
sensirion-ble==0.1.1
|
||||
@@ -2890,7 +2890,7 @@ tessie-api==0.1.1
|
||||
# tf-models-official==2.5.0
|
||||
|
||||
# homeassistant.components.thermobeacon
|
||||
thermobeacon-ble==0.8.0
|
||||
thermobeacon-ble==0.8.1
|
||||
|
||||
# homeassistant.components.thermopro
|
||||
thermopro-ble==0.11.0
|
||||
@@ -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
|
||||
|
||||
32
requirements_test_all.txt
generated
32
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
|
||||
@@ -249,7 +249,7 @@ aioharmony==0.4.1
|
||||
aiohasupervisor==0.3.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
aiohomeconnect==0.15.1
|
||||
aiohomeconnect==0.16.3
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.8
|
||||
@@ -404,10 +404,10 @@ aiowaqi==3.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webdav
|
||||
aiowebdav2==0.3.1
|
||||
aiowebdav2==0.4.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.7.1
|
||||
aiowebostv==0.7.3
|
||||
|
||||
# homeassistant.components.withings
|
||||
aiowithings==3.1.6
|
||||
@@ -892,7 +892,7 @@ google-cloud-texttospeech==2.17.2
|
||||
google-genai==1.1.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==7.1.3
|
||||
google-nest-sdm==7.1.4
|
||||
|
||||
# homeassistant.components.google_photos
|
||||
google-photos-library-api==0.12.1
|
||||
@@ -978,13 +978,13 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.67
|
||||
holidays==0.68
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250228.0
|
||||
home-assistant-frontend==20250306.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.0.9
|
||||
nexia==2.2.2
|
||||
|
||||
# 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
|
||||
@@ -1453,7 +1453,7 @@ py-nightscout==1.2.2
|
||||
py-sucks==0.9.10
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.7.0
|
||||
py-synologydsm-api==2.7.1
|
||||
|
||||
# homeassistant.components.hdmi_cec
|
||||
pyCEC==0.5.2
|
||||
@@ -1882,7 +1882,7 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==2.4.1
|
||||
pysmartthings==2.7.0
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@@ -2000,7 +2000,7 @@ python-roborock==2.11.1
|
||||
python-smarttub==0.0.39
|
||||
|
||||
# homeassistant.components.snoo
|
||||
python-snoo==0.6.0
|
||||
python-snoo==0.6.1
|
||||
|
||||
# homeassistant.components.songpal
|
||||
python-songpal==0.16.2
|
||||
@@ -2173,7 +2173,7 @@ securetar==2025.2.1
|
||||
|
||||
# homeassistant.components.emulated_kasa
|
||||
# homeassistant.components.sense
|
||||
sense-energy==0.13.5
|
||||
sense-energy==0.13.6
|
||||
|
||||
# homeassistant.components.sensirion_ble
|
||||
sensirion-ble==0.1.1
|
||||
@@ -2327,7 +2327,7 @@ teslemetry-stream==0.6.10
|
||||
tessie-api==0.1.1
|
||||
|
||||
# homeassistant.components.thermobeacon
|
||||
thermobeacon-ble==0.8.0
|
||||
thermobeacon-ble==0.8.1
|
||||
|
||||
# homeassistant.components.thermopro
|
||||
thermopro-ble==0.11.0
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from freezegun import freeze_time
|
||||
@@ -448,7 +449,7 @@ async def test_list_events_service(
|
||||
service: str,
|
||||
expected: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test listing events from the service call using exlplicit start and end time.
|
||||
"""Test listing events from the service call using explicit start and end time.
|
||||
|
||||
This test uses a fixed date/time so that it can deterministically test the
|
||||
string output values.
|
||||
@@ -553,3 +554,53 @@ async def test_list_events_missing_fields(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"frozen_time", ["2023-06-22 10:30:00+00:00"], ids=["frozen_time"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "error_msg"),
|
||||
[
|
||||
(
|
||||
{
|
||||
"start_date_time": "2023-06-22T04:30:00-06:00",
|
||||
"end_date_time": "2023-06-22T04:30:00-06:00",
|
||||
},
|
||||
"Expected end time to be after start time (2023-06-22 04:30:00-06:00, 2023-06-22 04:30:00-06:00)",
|
||||
),
|
||||
(
|
||||
{
|
||||
"start_date_time": "2023-06-22T04:30:00",
|
||||
"end_date_time": "2023-06-22T04:30:00",
|
||||
},
|
||||
"Expected end time to be after start time (2023-06-22 04:30:00, 2023-06-22 04:30:00)",
|
||||
),
|
||||
(
|
||||
{"start_date_time": "2023-06-22", "end_date_time": "2023-06-22"},
|
||||
"Expected end time to be after start time (2023-06-22 00:00:00, 2023-06-22 00:00:00)",
|
||||
),
|
||||
(
|
||||
{"start_date_time": "2023-06-22 10:00:00", "duration": "0"},
|
||||
"Expected positive duration (0:00:00)",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_list_events_service_same_dates(
|
||||
hass: HomeAssistant,
|
||||
service_data: dict[str, str],
|
||||
error_msg: str,
|
||||
) -> None:
|
||||
"""Test listing events from the service call using the same start and end time."""
|
||||
|
||||
with pytest.raises(vol.error.MultipleInvalid, match=re.escape(error_msg)):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GET_EVENTS,
|
||||
service_data={
|
||||
"entity_id": "calendar.calendar_1",
|
||||
**service_data,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
@@ -86,7 +86,9 @@ async def test_default_content(
|
||||
with (
|
||||
chat_session.async_get_chat_session(hass) as session,
|
||||
async_get_chat_log(hass, session, mock_conversation_input) as chat_log,
|
||||
async_get_chat_log(hass, session, mock_conversation_input) as chat_log2,
|
||||
):
|
||||
assert chat_log is chat_log2
|
||||
assert len(chat_log.content) == 2
|
||||
assert chat_log.content[0].role == "system"
|
||||
assert chat_log.content[0].content == ""
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.components.esphome.const import (
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
DOMAIN,
|
||||
STABLE_BLE_URL_VERSION,
|
||||
STABLE_BLE_VERSION_STR,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@@ -365,7 +366,7 @@ async def test_esphome_device_with_old_bluetooth(
|
||||
)
|
||||
assert (
|
||||
issue.learn_more_url
|
||||
== f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html"
|
||||
== f"https://esphome.io/changelog/{STABLE_BLE_URL_VERSION}.html"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -48,18 +48,18 @@ def location_status_fixture(install: str, loc_id: str | None = None) -> JsonObje
|
||||
return load_json_object_fixture(f"{install}/status_{loc_id}.json", DOMAIN)
|
||||
|
||||
|
||||
def dhw_schedule_fixture(install: str) -> JsonObjectType:
|
||||
def dhw_schedule_fixture(install: str, dhw_id: str | None = None) -> JsonObjectType:
|
||||
"""Load JSON for the schedule of a domesticHotWater zone."""
|
||||
try:
|
||||
return load_json_object_fixture(f"{install}/schedule_dhw.json", DOMAIN)
|
||||
return load_json_object_fixture(f"{install}/schedule_{dhw_id}.json", DOMAIN)
|
||||
except FileNotFoundError:
|
||||
return load_json_object_fixture("default/schedule_dhw.json", DOMAIN)
|
||||
|
||||
|
||||
def zone_schedule_fixture(install: str) -> JsonObjectType:
|
||||
def zone_schedule_fixture(install: str, zon_id: str | None = None) -> JsonObjectType:
|
||||
"""Load JSON for the schedule of a temperatureZone zone."""
|
||||
try:
|
||||
return load_json_object_fixture(f"{install}/schedule_zone.json", DOMAIN)
|
||||
return load_json_object_fixture(f"{install}/schedule_{zon_id}.json", DOMAIN)
|
||||
except FileNotFoundError:
|
||||
return load_json_object_fixture("default/schedule_zone.json", DOMAIN)
|
||||
|
||||
@@ -120,9 +120,9 @@ def mock_make_request(install: str) -> Callable:
|
||||
|
||||
elif "schedule" in url:
|
||||
if url.startswith("domesticHotWater"): # /v2/domesticHotWater/{id}/schedule
|
||||
return dhw_schedule_fixture(install)
|
||||
return dhw_schedule_fixture(install, url[16:23])
|
||||
if url.startswith("temperatureZone"): # /v2/temperatureZone/{id}/schedule
|
||||
return zone_schedule_fixture(install)
|
||||
return zone_schedule_fixture(install, url[16:23])
|
||||
|
||||
pytest.fail(f"Unexpected request: {HTTPMethod.GET} {url}")
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ TEST_INSTALLS: Final = (
|
||||
"default", # evohome: multi-zone, with DHW
|
||||
"h032585", # VisionProWifi: no preset modes for TCS, zoneId=systemId
|
||||
"h099625", # RoundThermostat
|
||||
"h139906", # zone with null schedule
|
||||
"sys_004", # RoundModulation
|
||||
)
|
||||
# "botched", # as default: but with activeFaults, ghost zones & unknown types
|
||||
|
||||
TEST_INSTALLS_WITH_DHW: Final = ("default",)
|
||||
TEST_INSTALLS_WITH_DHW: Final = ("default", "botched")
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dailySchedules": []
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dailySchedules": []
|
||||
}
|
||||
143
tests/components/evohome/fixtures/h139906/schedule_3454855.json
Normal file
143
tests/components/evohome/fixtures/h139906/schedule_3454855.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"dailySchedules": [
|
||||
{
|
||||
"dayOfWeek": "Monday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 22.0,
|
||||
"timeOfDay": "05:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 22.5,
|
||||
"timeOfDay": "16:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Tuesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 22.0,
|
||||
"timeOfDay": "05:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 22.5,
|
||||
"timeOfDay": "16:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Wednesday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 22.0,
|
||||
"timeOfDay": "05:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 22.5,
|
||||
"timeOfDay": "12:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Thursday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 22.0,
|
||||
"timeOfDay": "05:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 22.5,
|
||||
"timeOfDay": "16:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Friday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 22.0,
|
||||
"timeOfDay": "05:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 20.0,
|
||||
"timeOfDay": "08:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 22.5,
|
||||
"timeOfDay": "16:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Saturday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 22.0,
|
||||
"timeOfDay": "07:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 22.5,
|
||||
"timeOfDay": "16:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"dayOfWeek": "Sunday",
|
||||
"switchpoints": [
|
||||
{
|
||||
"heatSetpoint": 22.0,
|
||||
"timeOfDay": "07:30:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 22.5,
|
||||
"timeOfDay": "16:00:00"
|
||||
},
|
||||
{
|
||||
"heatSetpoint": 15.0,
|
||||
"timeOfDay": "23:00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"locationId": "2727366",
|
||||
"gateways": [
|
||||
{
|
||||
"gatewayId": "2513794",
|
||||
"temperatureControlSystems": [
|
||||
{
|
||||
"systemId": "3454856",
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": "3454854",
|
||||
"temperatureStatus": {
|
||||
"temperature": 22.0,
|
||||
"isAvailable": true
|
||||
},
|
||||
"activeFaults": [
|
||||
{
|
||||
"faultType": "TempZoneSensorCommunicationLost",
|
||||
"since": "2025-02-06T11:20:29"
|
||||
}
|
||||
],
|
||||
"setpointStatus": {
|
||||
"targetHeatTemperature": 5.0,
|
||||
"setpointMode": "FollowSchedule"
|
||||
},
|
||||
"name": "Thermostat"
|
||||
},
|
||||
{
|
||||
"zoneId": "3454855",
|
||||
"temperatureStatus": {
|
||||
"temperature": 22.0,
|
||||
"isAvailable": true
|
||||
},
|
||||
"activeFaults": [],
|
||||
"setpointStatus": {
|
||||
"targetHeatTemperature": 20.0,
|
||||
"setpointMode": "FollowSchedule"
|
||||
},
|
||||
"name": "Thermostat 2"
|
||||
}
|
||||
],
|
||||
"activeFaults": [],
|
||||
"systemModeStatus": {
|
||||
"mode": "Auto",
|
||||
"isPermanent": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"activeFaults": []
|
||||
}
|
||||
]
|
||||
}
|
||||
125
tests/components/evohome/fixtures/h139906/user_locations.json
Normal file
125
tests/components/evohome/fixtures/h139906/user_locations.json
Normal file
@@ -0,0 +1,125 @@
|
||||
[
|
||||
{
|
||||
"locationInfo": {
|
||||
"locationId": "2727366",
|
||||
"name": "Vr**********",
|
||||
"streetAddress": "********** *",
|
||||
"city": "*********",
|
||||
"country": "Netherlands",
|
||||
"postcode": "******",
|
||||
"locationType": "Residential",
|
||||
"useDaylightSaveSwitching": true,
|
||||
"timeZone": {
|
||||
"timeZoneId": "WEuropeStandardTime",
|
||||
"displayName": "(UTC+01:00) Amsterdam, Berlijn, Bern, Rome, Stockholm, Wenen",
|
||||
"offsetMinutes": 60,
|
||||
"currentOffsetMinutes": 60,
|
||||
"supportsDaylightSaving": true
|
||||
},
|
||||
"locationOwner": {
|
||||
"userId": "2276512",
|
||||
"username": "nobody@nowhere.com",
|
||||
"firstname": "Gl***",
|
||||
"lastname": "de*****"
|
||||
}
|
||||
},
|
||||
"gateways": [
|
||||
{
|
||||
"gatewayInfo": {
|
||||
"gatewayId": "2513794",
|
||||
"mac": "************",
|
||||
"crc": "****",
|
||||
"isWiFi": false
|
||||
},
|
||||
"temperatureControlSystems": [
|
||||
{
|
||||
"systemId": "3454856",
|
||||
"modelType": "EvoTouch",
|
||||
"zones": [
|
||||
{
|
||||
"zoneId": "3454854",
|
||||
"modelType": "HeatingZone",
|
||||
"setpointCapabilities": {
|
||||
"maxHeatSetpoint": 35.0,
|
||||
"minHeatSetpoint": 5.0,
|
||||
"valueResolution": 0.5,
|
||||
"canControlHeat": true,
|
||||
"canControlCool": false,
|
||||
"allowedSetpointModes": [
|
||||
"PermanentOverride",
|
||||
"FollowSchedule",
|
||||
"TemporaryOverride"
|
||||
],
|
||||
"maxDuration": "1.00:00:00",
|
||||
"timingResolution": "00:10:00"
|
||||
},
|
||||
"scheduleCapabilities": {
|
||||
"maxSwitchpointsPerDay": 6,
|
||||
"minSwitchpointsPerDay": 1,
|
||||
"timingResolution": "00:10:00",
|
||||
"setpointValueResolution": 0.5
|
||||
},
|
||||
"name": "Thermostat",
|
||||
"zoneType": "ZoneTemperatureControl"
|
||||
},
|
||||
{
|
||||
"zoneId": "3454855",
|
||||
"modelType": "RoundWireless",
|
||||
"setpointCapabilities": {
|
||||
"maxHeatSetpoint": 35.0,
|
||||
"minHeatSetpoint": 5.0,
|
||||
"valueResolution": 0.5,
|
||||
"canControlHeat": true,
|
||||
"canControlCool": false,
|
||||
"allowedSetpointModes": [
|
||||
"PermanentOverride",
|
||||
"FollowSchedule",
|
||||
"TemporaryOverride"
|
||||
],
|
||||
"maxDuration": "1.00:00:00",
|
||||
"timingResolution": "00:10:00"
|
||||
},
|
||||
"scheduleCapabilities": {
|
||||
"maxSwitchpointsPerDay": 6,
|
||||
"minSwitchpointsPerDay": 0,
|
||||
"timingResolution": "00:10:00",
|
||||
"setpointValueResolution": 0.5
|
||||
},
|
||||
"name": "Thermostat 2",
|
||||
"zoneType": "Thermostat"
|
||||
}
|
||||
],
|
||||
"allowedSystemModes": [
|
||||
{
|
||||
"systemMode": "Auto",
|
||||
"canBePermanent": true,
|
||||
"canBeTemporary": false
|
||||
},
|
||||
{
|
||||
"systemMode": "AutoWithEco",
|
||||
"canBePermanent": true,
|
||||
"canBeTemporary": true,
|
||||
"maxDuration": "1.00:00:00",
|
||||
"timingResolution": "01:00:00",
|
||||
"timingMode": "Duration"
|
||||
},
|
||||
{
|
||||
"systemMode": "Away",
|
||||
"canBePermanent": true,
|
||||
"canBeTemporary": true,
|
||||
"maxDuration": "99.00:00:00",
|
||||
"timingResolution": "1.00:00:00",
|
||||
"timingMode": "Period"
|
||||
},
|
||||
{
|
||||
"systemMode": "HeatingOff",
|
||||
"canBePermanent": true,
|
||||
"canBeTemporary": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -29,6 +29,16 @@
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_ctl_set_hvac_mode[h139906]
|
||||
list([
|
||||
tuple(
|
||||
<SystemMode.HEATING_OFF: 'HeatingOff'>,
|
||||
),
|
||||
tuple(
|
||||
<SystemMode.AUTO: 'Auto'>,
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_ctl_set_hvac_mode[minimal]
|
||||
list([
|
||||
tuple(
|
||||
@@ -70,6 +80,13 @@
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_ctl_turn_off[h139906]
|
||||
list([
|
||||
tuple(
|
||||
<SystemMode.HEATING_OFF: 'HeatingOff'>,
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_ctl_turn_off[minimal]
|
||||
list([
|
||||
tuple(
|
||||
@@ -105,6 +122,13 @@
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_ctl_turn_on[h139906]
|
||||
list([
|
||||
tuple(
|
||||
<SystemMode.AUTO: 'Auto'>,
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_ctl_turn_on[minimal]
|
||||
list([
|
||||
tuple(
|
||||
@@ -1118,6 +1142,136 @@
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[h139906][climate.thermostat-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 22.0,
|
||||
'friendly_name': 'Thermostat',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 5.0,
|
||||
'preset_mode': 'none',
|
||||
'preset_modes': list([
|
||||
'none',
|
||||
'temporary',
|
||||
'permanent',
|
||||
]),
|
||||
'status': dict({
|
||||
'activeFaults': tuple(
|
||||
dict({
|
||||
'fault_type': 'TempZoneSensorCommunicationLost',
|
||||
'since': '2025-02-06T11:20:29+01:00',
|
||||
}),
|
||||
),
|
||||
'setpoint_status': dict({
|
||||
'setpoint_mode': 'FollowSchedule',
|
||||
'target_heat_temperature': 5.0,
|
||||
}),
|
||||
'setpoints': dict({
|
||||
}),
|
||||
'temperature_status': dict({
|
||||
'is_available': True,
|
||||
'temperature': 22.0,
|
||||
}),
|
||||
'zone_id': '3454854',
|
||||
}),
|
||||
'supported_features': <ClimateEntityFeature: 401>,
|
||||
'temperature': 5.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.thermostat',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[h139906][climate.thermostat_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 22.0,
|
||||
'friendly_name': 'Thermostat 2',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'max_temp': 35.0,
|
||||
'min_temp': 5.0,
|
||||
'preset_mode': 'none',
|
||||
'preset_modes': list([
|
||||
'none',
|
||||
'temporary',
|
||||
'permanent',
|
||||
]),
|
||||
'status': dict({
|
||||
'activeFaults': tuple(
|
||||
),
|
||||
'setpoint_status': dict({
|
||||
'setpoint_mode': 'FollowSchedule',
|
||||
'target_heat_temperature': 20.0,
|
||||
}),
|
||||
'setpoints': dict({
|
||||
'next_sp_from': HAFakeDatetime(2024, 7, 10, 23, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin')),
|
||||
'next_sp_temp': 15.0,
|
||||
'this_sp_from': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='Europe/Berlin')),
|
||||
'this_sp_temp': 22.5,
|
||||
}),
|
||||
'temperature_status': dict({
|
||||
'is_available': True,
|
||||
'temperature': 22.0,
|
||||
}),
|
||||
'zone_id': '3454855',
|
||||
}),
|
||||
'supported_features': <ClimateEntityFeature: 401>,
|
||||
'temperature': 20.0,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.thermostat_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[h139906][climate.vr-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 22.0,
|
||||
'friendly_name': 'Vr**********',
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
]),
|
||||
'icon': 'mdi:thermostat',
|
||||
'max_temp': 35,
|
||||
'min_temp': 7,
|
||||
'preset_mode': None,
|
||||
'preset_modes': list([
|
||||
'eco',
|
||||
'away',
|
||||
]),
|
||||
'status': dict({
|
||||
'activeSystemFaults': tuple(
|
||||
),
|
||||
'system_id': '3454856',
|
||||
'system_mode_status': dict({
|
||||
'is_permanent': True,
|
||||
'mode': 'Auto',
|
||||
}),
|
||||
}),
|
||||
'supported_features': <ClimateEntityFeature: 400>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.vr',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup_platform[minimal][climate.main_room-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
@@ -1312,6 +1466,13 @@
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_set_hvac_mode[h139906]
|
||||
list([
|
||||
tuple(
|
||||
5.0,
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_set_hvac_mode[minimal]
|
||||
list([
|
||||
tuple(
|
||||
@@ -1365,6 +1526,19 @@
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_set_preset_mode[h139906]
|
||||
list([
|
||||
tuple(
|
||||
5.0,
|
||||
),
|
||||
tuple(
|
||||
5.0,
|
||||
),
|
||||
dict({
|
||||
'until': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_set_preset_mode[minimal]
|
||||
list([
|
||||
tuple(
|
||||
@@ -1412,6 +1586,13 @@
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_set_temperature[h139906]
|
||||
list([
|
||||
dict({
|
||||
'until': None,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_set_temperature[minimal]
|
||||
list([
|
||||
dict({
|
||||
@@ -1447,6 +1628,13 @@
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_turn_off[h139906]
|
||||
list([
|
||||
tuple(
|
||||
5.0,
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_zone_turn_off[minimal]
|
||||
list([
|
||||
tuple(
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
# name: test_setup[h099625]
|
||||
dict_keys(['refresh_system', 'set_system_mode', 'clear_zone_override', 'set_zone_override'])
|
||||
# ---
|
||||
# name: test_setup[h139906]
|
||||
dict_keys(['refresh_system', 'set_system_mode', 'clear_zone_override', 'set_zone_override'])
|
||||
# ---
|
||||
# name: test_setup[minimal]
|
||||
dict_keys(['refresh_system', 'reset_system', 'set_system_mode', 'clear_zone_override', 'set_zone_override'])
|
||||
# ---
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
# serializer version: 1
|
||||
# name: test_set_operation_mode[botched]
|
||||
list([
|
||||
dict({
|
||||
'until': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
dict({
|
||||
'until': HAFakeDatetime(2024, 7, 10, 12, 0, tzinfo=datetime.timezone.utc),
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_set_operation_mode[default]
|
||||
list([
|
||||
dict({
|
||||
|
||||
@@ -33,7 +33,7 @@ from .const import TEST_INSTALLS_WITH_DHW
|
||||
DHW_ENTITY_ID = "water_heater.domestic_hot_water"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("install", [*TEST_INSTALLS_WITH_DHW, "botched"])
|
||||
@pytest.mark.parametrize("install", TEST_INSTALLS_WITH_DHW)
|
||||
async def test_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, str],
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
tuple(
|
||||
),
|
||||
dict({
|
||||
'config': GenerateContentConfig(http_options=None, system_instruction="Current time is 05:00:00. Today's date is 2024-05-24.\nYou are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nOnly if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.", temperature=1.0, top_p=0.95, top_k=64.0, candidate_count=None, max_output_tokens=150, stop_sequences=None, response_logprobs=None, logprobs=None, presence_penalty=None, frequency_penalty=None, seed=None, response_mime_type=None, response_schema=None, routing_config=None, safety_settings=[SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>)], tools=[Tool(function_declarations=[FunctionDeclaration(response=None, description='Test function', name='test_tool', parameters=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'param1': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.ARRAY: 'ARRAY'>, description='Test parameters', enum=None, format=None, items=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None), properties=None, required=None), 'param2': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=[Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.NUMBER: 'NUMBER'>, description=None, enum=None, format=None, items=None, properties=None, required=None), Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.INTEGER: 'INTEGER'>, description=None, enum=None, format=None, items=None, properties=None, required=None)], max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=None, description=None, enum=None, format=None, items=None, properties=None, required=None), 'param3': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'json': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None)}, required=[])}, required=[]))], retrieval=None, google_search=None, google_search_retrieval=None, code_execution=None)], tool_config=None, labels=None, cached_content=None, response_modalities=None, media_resolution=None, speech_config=None, audio_timestamp=None, automatic_function_calling=AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None, ignore_call_history=None), thinking_config=None),
|
||||
'config': GenerateContentConfig(http_options=None, system_instruction="Current time is 05:00:00. Today's date is 2024-05-24.\nYou are a voice assistant for Home Assistant.\nAnswer questions about the world truthfully.\nAnswer in plain text. Keep it simple and to the point.\nOnly if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.", temperature=1.0, top_p=0.95, top_k=64.0, candidate_count=None, max_output_tokens=150, stop_sequences=None, response_logprobs=None, logprobs=None, presence_penalty=None, frequency_penalty=None, seed=None, response_mime_type=None, response_schema=None, routing_config=None, safety_settings=[SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HATE_SPEECH: 'HARM_CATEGORY_HATE_SPEECH'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_HARASSMENT: 'HARM_CATEGORY_HARASSMENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: 'HARM_CATEGORY_DANGEROUS_CONTENT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>), SafetySetting(method=None, category=<HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: 'HARM_CATEGORY_SEXUALLY_EXPLICIT'>, threshold=<HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE: 'BLOCK_MEDIUM_AND_ABOVE'>)], tools=[Tool(function_declarations=[FunctionDeclaration(response=None, description='Test function', name='test_tool', parameters=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'param1': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.ARRAY: 'ARRAY'>, description='Test parameters', enum=None, format=None, items=Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None), properties=None, required=None), 'param2': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=None, description=None, enum=None, format=None, items=None, properties=None, required=None), 'param3': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.OBJECT: 'OBJECT'>, description=None, enum=None, format=None, items=None, properties={'json': Schema(min_items=None, example=None, property_ordering=None, pattern=None, minimum=None, default=None, any_of=None, max_length=None, title=None, min_length=None, min_properties=None, max_items=None, maximum=None, nullable=None, max_properties=None, type=<Type.STRING: 'STRING'>, description=None, enum=None, format=None, items=None, properties=None, required=None)}, required=[])}, required=[]))], retrieval=None, google_search=None, google_search_retrieval=None, code_execution=None)], tool_config=None, labels=None, cached_content=None, response_modalities=None, media_resolution=None, speech_config=None, audio_timestamp=None, automatic_function_calling=AutomaticFunctionCallingConfig(disable=True, maximum_remote_calls=None, ignore_call_history=None), thinking_config=None),
|
||||
'history': list([
|
||||
]),
|
||||
'model': 'models/gemini-2.0-flash',
|
||||
|
||||
@@ -31,3 +31,18 @@
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_load_entry_with_unloaded_entries
|
||||
list([
|
||||
tuple(
|
||||
'',
|
||||
tuple(
|
||||
),
|
||||
dict({
|
||||
'contents': list([
|
||||
'Write an opening speech for a Home Assistant release party',
|
||||
]),
|
||||
'model': 'models/gemini-2.0-flash',
|
||||
}),
|
||||
),
|
||||
])
|
||||
# ---
|
||||
|
||||
@@ -493,6 +493,26 @@ async def test_escape_decode() -> None:
|
||||
{"type": "string", "enum": ["a", "b", "c"]},
|
||||
{"type": "STRING", "enum": ["a", "b", "c"]},
|
||||
),
|
||||
(
|
||||
{"type": "string", "default": "default"},
|
||||
{"type": "STRING"},
|
||||
),
|
||||
(
|
||||
{"type": "string", "pattern": "default"},
|
||||
{"type": "STRING"},
|
||||
),
|
||||
(
|
||||
{"type": "string", "maxLength": 10},
|
||||
{"type": "STRING"},
|
||||
),
|
||||
(
|
||||
{"type": "string", "minLength": 10},
|
||||
{"type": "STRING"},
|
||||
),
|
||||
(
|
||||
{"type": "string", "title": "title"},
|
||||
{"type": "STRING"},
|
||||
),
|
||||
(
|
||||
{"type": "string", "format": "enum", "enum": ["a", "b", "c"]},
|
||||
{"type": "STRING", "format": "enum", "enum": ["a", "b", "c"]},
|
||||
@@ -517,6 +537,10 @@ async def test_escape_decode() -> None:
|
||||
{"type": "number", "format": "hex"},
|
||||
{"type": "NUMBER"},
|
||||
),
|
||||
(
|
||||
{"type": "number", "minimum": 1},
|
||||
{"type": "NUMBER"},
|
||||
),
|
||||
(
|
||||
{"type": "integer", "format": "int32"},
|
||||
{"type": "INTEGER", "format": "int32"},
|
||||
@@ -535,21 +559,7 @@ async def test_escape_decode() -> None:
|
||||
),
|
||||
(
|
||||
{"anyOf": [{"type": "integer"}, {"type": "number"}]},
|
||||
{"any_of": [{"type": "INTEGER"}, {"type": "NUMBER"}]},
|
||||
),
|
||||
(
|
||||
{
|
||||
"any_of": [
|
||||
{"any_of": [{"type": "integer"}, {"type": "number"}]},
|
||||
{"any_of": [{"type": "integer"}, {"type": "number"}]},
|
||||
]
|
||||
},
|
||||
{
|
||||
"any_of": [
|
||||
{"any_of": [{"type": "INTEGER"}, {"type": "NUMBER"}]},
|
||||
{"any_of": [{"type": "INTEGER"}, {"type": "NUMBER"}]},
|
||||
]
|
||||
},
|
||||
{},
|
||||
),
|
||||
({"type": "string", "format": "lower"}, {"type": "STRING"}),
|
||||
({"type": "boolean", "format": "bool"}, {"type": "BOOLEAN"}),
|
||||
@@ -570,7 +580,15 @@ async def test_escape_decode() -> None:
|
||||
},
|
||||
),
|
||||
(
|
||||
{"type": "object", "additionalProperties": True},
|
||||
{"type": "object", "additionalProperties": True, "minProperties": 1},
|
||||
{
|
||||
"type": "OBJECT",
|
||||
"properties": {"json": {"type": "STRING"}},
|
||||
"required": [],
|
||||
},
|
||||
),
|
||||
(
|
||||
{"type": "object", "additionalProperties": True, "maxProperties": 1},
|
||||
{
|
||||
"type": "OBJECT",
|
||||
"properties": {"json": {"type": "STRING"}},
|
||||
@@ -581,6 +599,20 @@ async def test_escape_decode() -> None:
|
||||
{"type": "array", "items": {"type": "string"}},
|
||||
{"type": "ARRAY", "items": {"type": "STRING"}},
|
||||
),
|
||||
(
|
||||
{
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"minItems": 1,
|
||||
"maxItems": 2,
|
||||
},
|
||||
{
|
||||
"type": "ARRAY",
|
||||
"items": {"type": "STRING"},
|
||||
"min_items": 1,
|
||||
"max_items": 2,
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_format_schema(openapi, genai_schema) -> None:
|
||||
|
||||
@@ -224,3 +224,52 @@ async def test_config_entry_error(
|
||||
await hass.async_block_till_done()
|
||||
assert mock_config_entry.state == state
|
||||
assert any(mock_config_entry.async_get_active_flows(hass, {"reauth"})) == reauth
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
async def test_load_entry_with_unloaded_entries(
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
"""Test loading an entry with unloaded entries."""
|
||||
config_entries = hass.config_entries.async_entries(
|
||||
"google_generative_ai_conversation"
|
||||
)
|
||||
runtime_data = config_entries[0].runtime_data
|
||||
await hass.config_entries.async_unload(config_entries[0].entry_id)
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain="google_generative_ai_conversation",
|
||||
title="Google Generative AI Conversation",
|
||||
data={
|
||||
"api_key": "bla",
|
||||
},
|
||||
state=ConfigEntryState.LOADED,
|
||||
)
|
||||
entry.runtime_data = runtime_data
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
stubbed_generated_content = (
|
||||
"I'm thrilled to welcome you all to the release "
|
||||
"party for the latest version of Home Assistant!"
|
||||
)
|
||||
|
||||
with patch(
|
||||
"google.genai.models.AsyncModels.generate_content",
|
||||
return_value=Mock(
|
||||
text=stubbed_generated_content,
|
||||
prompt_feedback=None,
|
||||
candidates=[Mock()],
|
||||
),
|
||||
) as mock_generate:
|
||||
response = await hass.services.async_call(
|
||||
"google_generative_ai_conversation",
|
||||
"generate_content",
|
||||
{"prompt": "Write an opening speech for a Home Assistant release party"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert response == {
|
||||
"text": stubbed_generated_content,
|
||||
}
|
||||
assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot
|
||||
|
||||
@@ -49,7 +49,7 @@ async def test_holiday_calendar_entity(
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.united_states_ak",
|
||||
"end_date_time": dt_util.now(),
|
||||
"end_date_time": dt_util.now() + timedelta(hours=1),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
@@ -135,7 +135,7 @@ async def test_default_language(
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.france_bl",
|
||||
"end_date_time": dt_util.now(),
|
||||
"end_date_time": dt_util.now() + timedelta(hours=1),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
@@ -164,7 +164,7 @@ async def test_default_language(
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.france_bl",
|
||||
"end_date_time": dt_util.now(),
|
||||
"end_date_time": dt_util.now() + timedelta(hours=1),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
@@ -211,7 +211,7 @@ async def test_no_language(
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.albania",
|
||||
"end_date_time": dt_util.now(),
|
||||
"end_date_time": dt_util.now() + timedelta(hours=1),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
@@ -308,7 +308,7 @@ async def test_language_not_exist(
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.norge",
|
||||
"end_date_time": dt_util.now(),
|
||||
"end_date_time": dt_util.now() + timedelta(hours=1),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
@@ -336,7 +336,7 @@ async def test_language_not_exist(
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
"entity_id": "calendar.norge",
|
||||
"end_date_time": dt_util.now(),
|
||||
"end_date_time": dt_util.now() + timedelta(hours=1),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
|
||||
@@ -1 +1,19 @@
|
||||
"""Tests for the Home Connect integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohomeconnect.model import ArrayOfHomeAppliances, ArrayOfStatus
|
||||
|
||||
from tests.common import load_json_object_fixture
|
||||
|
||||
MOCK_APPLIANCES = ArrayOfHomeAppliances.from_dict(
|
||||
load_json_object_fixture("home_connect/appliances.json")["data"] # type: ignore[arg-type]
|
||||
)
|
||||
MOCK_PROGRAMS: dict[str, Any] = load_json_object_fixture("home_connect/programs.json")
|
||||
MOCK_SETTINGS: dict[str, Any] = load_json_object_fixture("home_connect/settings.json")
|
||||
MOCK_STATUS = ArrayOfStatus.from_dict(
|
||||
load_json_object_fixture("home_connect/status.json")["data"] # type: ignore[arg-type]
|
||||
)
|
||||
MOCK_AVAILABLE_COMMANDS: dict[str, Any] = load_json_object_fixture(
|
||||
"home_connect/available_commands.json"
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user