mirror of
https://github.com/home-assistant/core.git
synced 2025-12-14 03:48:30 +00:00
Compare commits
64 Commits
input_bool
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8ab0bcadf | ||
|
|
19cb827577 | ||
|
|
03676d7e5a | ||
|
|
13f3b49b96 | ||
|
|
90c8c56a06 | ||
|
|
afb9e18a7d | ||
|
|
2c2934065f | ||
|
|
0bead67df9 | ||
|
|
2895849203 | ||
|
|
b2400708ac | ||
|
|
0bed9c20b3 | ||
|
|
d3fb7a7b87 | ||
|
|
60dcca4143 | ||
|
|
01f498f239 | ||
|
|
15055b8e8e | ||
|
|
6826619e12 | ||
|
|
b50a8e04a8 | ||
|
|
c6c67c5357 | ||
|
|
c82803d1e2 | ||
|
|
732b30f181 | ||
|
|
0e2e57a657 | ||
|
|
f00b0080a9 | ||
|
|
ad970c1234 | ||
|
|
02ec56bffa | ||
|
|
8388c290bf | ||
|
|
576ee99faf | ||
|
|
8a3534c345 | ||
|
|
e1e91c5568 | ||
|
|
1e09bddb1d | ||
|
|
90e4340595 | ||
|
|
120b17349c | ||
|
|
8a26961304 | ||
|
|
407b675080 | ||
|
|
274844271b | ||
|
|
f11e4e7bda | ||
|
|
96f8c39c6f | ||
|
|
77b79fef8d | ||
|
|
a0d2f285f3 | ||
|
|
3aef05d1ec | ||
|
|
510e391ee4 | ||
|
|
54adfdd694 | ||
|
|
d45f920b4a | ||
|
|
3080ef9a4a | ||
|
|
51cebb52f3 | ||
|
|
7b0d4c47b7 | ||
|
|
a660ab3f97 | ||
|
|
dd8fc16788 | ||
|
|
2b0fab0468 | ||
|
|
3bb88ed433 | ||
|
|
984385cd98 | ||
|
|
09de108676 | ||
|
|
ebc7581718 | ||
|
|
e55162812d | ||
|
|
aa6ccaa024 | ||
|
|
e1b009a6de | ||
|
|
91ddc525b0 | ||
|
|
d7d7954ac2 | ||
|
|
e87c260df7 | ||
|
|
5185c6cd68 | ||
|
|
7599c918e2 | ||
|
|
fa7e22ec91 | ||
|
|
606519e51b | ||
|
|
8e39e010f7 | ||
|
|
dc01cf49a0 |
8
CODEOWNERS
generated
8
CODEOWNERS
generated
@@ -220,8 +220,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||
/homeassistant/components/blebox/ @bbx-a @swistakm
|
||||
/tests/components/blebox/ @bbx-a @swistakm
|
||||
/homeassistant/components/blink/ @fronzbot @mkmer
|
||||
/tests/components/blink/ @fronzbot @mkmer
|
||||
/homeassistant/components/blink/ @fronzbot
|
||||
/tests/components/blink/ @fronzbot
|
||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/homeassistant/components/bluemaestro/ @bdraco
|
||||
@@ -308,8 +308,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/config/ @home-assistant/core
|
||||
/homeassistant/components/configurator/ @home-assistant/core
|
||||
/tests/components/configurator/ @home-assistant/core
|
||||
/homeassistant/components/control4/ @lawtancool
|
||||
/tests/components/control4/ @lawtancool
|
||||
/homeassistant/components/control4/ @lawtancool @davidrecordon
|
||||
/tests/components/control4/ @lawtancool @davidrecordon
|
||||
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/homeassistant/components/cookidoo/ @miaucl
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@Bre77"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["advantage_air"],
|
||||
"requirements": ["advantage-air==0.4.4"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@Noltari"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.6.4"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aftership",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyaftership==21.11.0"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@ispysoftware"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["agent"],
|
||||
"requirements": ["agent-py==0.0.24"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@asymworks"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairnow"],
|
||||
"requirements": ["pyairnow==1.3.1"]
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from pyairobotrest.models import ThermostatStatus
|
||||
|
||||
@@ -23,6 +24,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.util.variance import ignore_variance
|
||||
|
||||
from . import AirobotConfigEntry
|
||||
from .entity import AirobotEntity
|
||||
@@ -34,10 +37,15 @@ PARALLEL_UPDATES = 0
|
||||
class AirobotSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Airobot sensor entity."""
|
||||
|
||||
value_fn: Callable[[ThermostatStatus], StateType]
|
||||
value_fn: Callable[[ThermostatStatus], StateType | datetime]
|
||||
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
|
||||
|
||||
|
||||
uptime_to_stable_datetime = ignore_variance(
|
||||
lambda value: utcnow().replace(microsecond=0) - timedelta(seconds=value),
|
||||
timedelta(minutes=2),
|
||||
)
|
||||
|
||||
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
|
||||
AirobotSensorEntityDescription(
|
||||
key="air_temperature",
|
||||
@@ -96,6 +104,14 @@ SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda status: status.errors,
|
||||
),
|
||||
AirobotSensorEntityDescription(
|
||||
key="device_uptime",
|
||||
translation_key="device_uptime",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda status: uptime_to_stable_datetime(status.device_uptime),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -129,6 +145,6 @@ class AirobotSensor(AirobotEntity, SensorEntity):
|
||||
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data.status)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["airthings"],
|
||||
"requirements": ["airthings-cloud==0.2.0"]
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airthings-ble==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@samsinnamon"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["airtouch4pyapi"],
|
||||
"requirements": ["airtouch4pyapi==1.0.5"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@danzel"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["airtouch5py"],
|
||||
"requirements": ["airtouch5py==0.3.0"]
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==1.0.4"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@Noltari"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.7.2"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@madpilot"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["amberelectric"],
|
||||
"requirements": ["amberelectric==2.0.12"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@engrbm87"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pydroid-ipcam==3.0.0"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@Lash-L"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["anova_wifi"],
|
||||
"requirements": ["anova-wifi==0.17.0"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@hyralex"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/anthemav",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["anthemav"],
|
||||
"requirements": ["anthemav==1.4.1"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@bdr99"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["py-aosmith==1.0.15"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@elupus"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.8.2"],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@ikalnyi"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/arve",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["asyncarve==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@milanmeu"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaseko"],
|
||||
"requirements": ["aioaseko==1.0.0"]
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
|
||||
"requirements": ["aioasuswrt==1.5.2", "asusrouter==1.21.3"]
|
||||
"requirements": ["aioasuswrt==1.5.3", "asusrouter==1.21.3"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@MatsNL"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/atag",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyatag"],
|
||||
"requirements": ["pyatag==0.3.5.3"]
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@djtimca"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aurora",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["auroranoaa"],
|
||||
"requirements": ["auroranoaa==0.0.5"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@nickw444", "@Bre77"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aussiebb"],
|
||||
"requirements": ["pyaussiebb==0.1.5"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@kaareseras"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["azure"],
|
||||
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@timmo001"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/azure_devops",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioazuredevops"],
|
||||
"requirements": ["aioazuredevops==2.2.2"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@eavanvalkenburg"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["azure"],
|
||||
"requirements": ["azure-eventhub==5.11.1"],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@bdraco", "@jfroy"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aiobafi6==0.9.0"],
|
||||
"zeroconf": [
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pybalboa"],
|
||||
"requirements": ["pybalboa==1.1.3"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@bbx-a", "@swistakm"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/blebox",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["blebox_uniapi"],
|
||||
"requirements": ["blebox-uniapi==2.5.0"],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "blink",
|
||||
"name": "Blink",
|
||||
"codeowners": ["@fronzbot", "@mkmer"],
|
||||
"codeowners": ["@fronzbot"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["blinkpy"],
|
||||
"requirements": ["blinkpy==0.25.1"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/blue_current",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["bluecurrent_api"],
|
||||
"requirements": ["bluecurrent-api==1.3.2"]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluemaestro-ble==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"codeowners": ["@thrawnarn", "@LouisChrist"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluesound",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pyblu==2.0.5"],
|
||||
"zeroconf": [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer-connected[china]==0.17.3"]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bond",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["bond_async"],
|
||||
"requirements": ["bond-async==0.2.1"],
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"codeowners": ["@tschamm"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_shc",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["boschshcpy"],
|
||||
"requirements": ["boschshcpy==0.2.107"],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@gjohansson-ST"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/brottsplatskartan",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["brottsplatskartan"],
|
||||
"requirements": ["brottsplatskartan==1.0.5"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@eavanvalkenburg"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/brunt",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["brunt"],
|
||||
"requirements": ["brunt==1.2.0"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@mjj4791", "@ties", "@Robbie1221"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/buienradar",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["buienradar", "vincenty"],
|
||||
"requirements": ["buienradar==1.0.6"]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==2.1.0", "icalendar==6.3.1", "vobject==0.9.9"]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/canary",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["canary"],
|
||||
"requirements": ["py-canary==0.5.4"],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@ocalvo"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ccm15",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["py_ccm15==0.1.2"]
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
"codeowners": ["@jjlawren"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/cert_expiry",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@ludeeus", "@ctalkington"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/cloudflare",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pycfdns"],
|
||||
"requirements": ["pycfdns==3.0.0"],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@tombrien"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/coinbase",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["coinbase"],
|
||||
"requirements": ["coinbase-advanced-py==1.2.2"]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"domain": "control4",
|
||||
"name": "Control4",
|
||||
"codeowners": ["@lawtancool"],
|
||||
"codeowners": ["@lawtancool", "@davidrecordon"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/control4",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyControl4"],
|
||||
"requirements": ["pyControl4==1.5.0"],
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@gjohansson-ST"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==3.6.0"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["http", "repairs"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["doorbirdpy"],
|
||||
"requirements": ["DoorBirdPy==3.0.11"],
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"homekit": {
|
||||
"models": ["EB", "ecobee*"]
|
||||
},
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyecobee"],
|
||||
"requirements": ["python-ecobee-api==0.3.2"],
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "The API key for authenticating with Firefly",
|
||||
"api_key": "The API key for authenticating with Firefly III",
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"verify_ssl": "Verify the SSL certificate of the Firefly instance"
|
||||
"verify_ssl": "Verify the SSL certificate of the Firefly III instance"
|
||||
},
|
||||
"description": "You can create an API key in the Firefly UI. Go to **Options > Profile** and select the **OAuth** tab. Create a new personal access token and copy it (it will only display once)."
|
||||
"description": "You can create an API key in the Firefly III UI. Go to **Options > Profile** and select the **OAuth** tab. Create a new personal access token and copy it (it will only display once)."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -84,13 +84,13 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "An error occurred while trying to connect to the Firefly instance: {error}"
|
||||
"message": "An error occurred while trying to connect to the Firefly III instance: {error}"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "An error occurred while trying to authenticate: {error}"
|
||||
},
|
||||
"timeout_connect": {
|
||||
"message": "A timeout occurred while trying to connect to the Firefly instance: {error}"
|
||||
"message": "A timeout occurred while trying to connect to the Firefly III instance: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"bbw_dose": {
|
||||
"default": "mdi:weight-gram"
|
||||
},
|
||||
"coffee_temp": {
|
||||
"default": "mdi:thermometer-water"
|
||||
},
|
||||
@@ -51,6 +54,14 @@
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"bbw_dose_mode": {
|
||||
"default": "mdi:all-inclusive-box",
|
||||
"state": {
|
||||
"continuous": "mdi:all-inclusive-box",
|
||||
"dose1": "mdi:numeric-1-box",
|
||||
"dose2": "mdi:numeric-2-box"
|
||||
}
|
||||
},
|
||||
"prebrew_infusion_select": {
|
||||
"default": "mdi:water-pump-off",
|
||||
"state": {
|
||||
|
||||
@@ -5,9 +5,14 @@ from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from pylamarzocco import LaMarzoccoMachine
|
||||
from pylamarzocco.const import ModelName, PreExtractionMode, WidgetType
|
||||
from pylamarzocco.const import DoseMode, ModelName, PreExtractionMode, WidgetType
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
from pylamarzocco.models import CoffeeBoiler, PreBrewing, SteamBoilerTemperature
|
||||
from pylamarzocco.models import (
|
||||
BrewByWeightDoses,
|
||||
CoffeeBoiler,
|
||||
PreBrewing,
|
||||
SteamBoilerTemperature,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
@@ -18,6 +23,7 @@ from homeassistant.const import (
|
||||
PRECISION_TENTHS,
|
||||
PRECISION_WHOLE,
|
||||
EntityCategory,
|
||||
UnitOfMass,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
@@ -219,6 +225,72 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
)
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="bbw_dose_1",
|
||||
translation_key="bbw_dose",
|
||||
translation_placeholders={"dose": "Dose 1"},
|
||||
device_class=NumberDeviceClass.WEIGHT,
|
||||
native_unit_of_measurement=UnitOfMass.GRAMS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=5,
|
||||
native_max_value=100,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_brew_by_weight_dose(
|
||||
dose=DoseMode.DOSE_1,
|
||||
value=value,
|
||||
)
|
||||
),
|
||||
native_value_fn=(
|
||||
lambda machine: cast(
|
||||
BrewByWeightDoses,
|
||||
machine.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
|
||||
).doses.dose_1.dose
|
||||
),
|
||||
available_fn=lambda coordinator: (
|
||||
cast(
|
||||
BrewByWeightDoses,
|
||||
coordinator.device.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
|
||||
).scale_connected
|
||||
),
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="bbw_dose_2",
|
||||
translation_key="bbw_dose",
|
||||
translation_placeholders={"dose": "Dose 2"},
|
||||
device_class=NumberDeviceClass.WEIGHT,
|
||||
native_unit_of_measurement=UnitOfMass.GRAMS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=5,
|
||||
native_max_value=100,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_brew_by_weight_dose(
|
||||
dose=DoseMode.DOSE_2,
|
||||
value=value,
|
||||
)
|
||||
),
|
||||
native_value_fn=(
|
||||
lambda machine: cast(
|
||||
BrewByWeightDoses,
|
||||
machine.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
|
||||
).doses.dose_2.dose
|
||||
),
|
||||
available_fn=lambda coordinator: (
|
||||
cast(
|
||||
BrewByWeightDoses,
|
||||
coordinator.device.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
|
||||
).scale_connected
|
||||
),
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from pylamarzocco.const import (
|
||||
DoseMode,
|
||||
ModelName,
|
||||
PreExtractionMode,
|
||||
SmartStandByType,
|
||||
@@ -13,7 +14,7 @@ from pylamarzocco.const import (
|
||||
)
|
||||
from pylamarzocco.devices import LaMarzoccoMachine
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
from pylamarzocco.models import PreBrewing, SteamBoilerLevel
|
||||
from pylamarzocco.models import BrewByWeightDoses, PreBrewing, SteamBoilerLevel
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
@@ -50,6 +51,14 @@ STANDBY_MODE_HA_TO_LM = {
|
||||
|
||||
STANDBY_MODE_LM_TO_HA = {value: key for key, value in STANDBY_MODE_HA_TO_LM.items()}
|
||||
|
||||
DOSE_MODE_HA_TO_LM = {
|
||||
"continuous": DoseMode.CONTINUOUS,
|
||||
"dose1": DoseMode.DOSE_1,
|
||||
"dose2": DoseMode.DOSE_2,
|
||||
}
|
||||
|
||||
DOSE_MODE_LM_TO_HA = {value: key for key, value in DOSE_MODE_HA_TO_LM.items()}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LaMarzoccoSelectEntityDescription(
|
||||
@@ -117,6 +126,31 @@ ENTITIES: tuple[LaMarzoccoSelectEntityDescription, ...] = (
|
||||
machine.schedule.smart_wake_up_sleep.smart_stand_by_after
|
||||
],
|
||||
),
|
||||
LaMarzoccoSelectEntityDescription(
|
||||
key="bbw_dose_mode",
|
||||
translation_key="bbw_dose_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options=["continuous", "dose1", "dose2"],
|
||||
select_option_fn=lambda machine, option: machine.set_brew_by_weight_dose_mode(
|
||||
mode=DOSE_MODE_HA_TO_LM[option]
|
||||
),
|
||||
current_option_fn=lambda machine: DOSE_MODE_LM_TO_HA[
|
||||
cast(
|
||||
BrewByWeightDoses,
|
||||
machine.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
|
||||
).mode
|
||||
],
|
||||
available_fn=lambda coordinator: (
|
||||
cast(
|
||||
BrewByWeightDoses,
|
||||
coordinator.device.dashboard.config[WidgetType.CM_BREW_BY_WEIGHT_DOSES],
|
||||
).scale_connected
|
||||
),
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.device.dashboard.model_name
|
||||
in (ModelName.LINEA_MINI, ModelName.LINEA_MINI_R)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,9 @@
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"bbw_dose": {
|
||||
"name": "Brew by weight {dose}"
|
||||
},
|
||||
"coffee_temp": {
|
||||
"name": "Coffee target temperature"
|
||||
},
|
||||
@@ -107,6 +110,14 @@
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"bbw_dose_mode": {
|
||||
"name": "Brew by weight dose mode",
|
||||
"state": {
|
||||
"continuous": "Continuous",
|
||||
"dose1": "Dose 1",
|
||||
"dose2": "Dose 2"
|
||||
}
|
||||
},
|
||||
"prebrew_infusion_select": {
|
||||
"name": "Prebrew/-infusion mode",
|
||||
"state": {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Support for LCN climate control."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
@@ -172,14 +171,14 @@ class LcnClimate(LcnEntity, ClimateEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Update the state of the entity."""
|
||||
self._attr_available = any(
|
||||
await asyncio.gather(
|
||||
self.device_connection.request_status_variable(
|
||||
[
|
||||
await self.device_connection.request_status_variable(
|
||||
self.variable, SCAN_INTERVAL.seconds
|
||||
),
|
||||
self.device_connection.request_status_variable(
|
||||
await self.device_connection.request_status_variable(
|
||||
self.setpoint, SCAN_INTERVAL.seconds
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def input_received(self, input_obj: InputType) -> None:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Support for LCN covers."""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Coroutine, Iterable
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
@@ -134,14 +133,14 @@ class LcnOutputsCover(LcnEntity, CoverEntity):
|
||||
"""Update the state of the entity."""
|
||||
if not self.device_connection.is_group:
|
||||
self._attr_available = any(
|
||||
await asyncio.gather(
|
||||
self.device_connection.request_status_output(
|
||||
[
|
||||
await self.device_connection.request_status_output(
|
||||
pypck.lcn_defs.OutputPort["OUTPUTUP"], SCAN_INTERVAL.seconds
|
||||
),
|
||||
self.device_connection.request_status_output(
|
||||
await self.device_connection.request_status_output(
|
||||
pypck.lcn_defs.OutputPort["OUTPUTDOWN"], SCAN_INTERVAL.seconds
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
def input_received(self, input_obj: InputType) -> None:
|
||||
@@ -274,7 +273,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
|
||||
self.motor, self.positioning_mode, SCAN_INTERVAL.seconds
|
||||
)
|
||||
)
|
||||
self._attr_available = any(await asyncio.gather(*coros))
|
||||
self._attr_available = any([await coro for coro in coros])
|
||||
|
||||
def input_received(self, input_obj: InputType) -> None:
|
||||
"""Set cover states when LCN input object (command) is received."""
|
||||
|
||||
@@ -15,6 +15,13 @@ from .entity import MealieEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
SUPPORTED_MEALPLAN_ENTRY_TYPES = [
|
||||
MealplanEntryType.BREAKFAST,
|
||||
MealplanEntryType.DINNER,
|
||||
MealplanEntryType.LUNCH,
|
||||
MealplanEntryType.SIDE,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -26,7 +33,7 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(
|
||||
MealieMealplanCalendarEntity(coordinator, entry_type)
|
||||
for entry_type in MealplanEntryType
|
||||
for entry_type in SUPPORTED_MEALPLAN_ENTRY_TYPES
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiomealie==1.1.0"]
|
||||
"requirements": ["aiomealie==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-overseerr==0.7.1"]
|
||||
"requirements": ["python-overseerr==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from plugwise.constants import GwEntityData
|
||||
from plugwise import GwEntityData
|
||||
|
||||
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
|
||||
from homeassistant.helpers.device_registry import (
|
||||
@@ -30,37 +30,43 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]):
|
||||
super().__init__(coordinator)
|
||||
self._dev_id = device_id
|
||||
|
||||
configuration_url: str | None = None
|
||||
if entry := self.coordinator.config_entry:
|
||||
configuration_url = f"http://{entry.data[CONF_HOST]}"
|
||||
api = coordinator.api
|
||||
gateway_id = api.gateway_id
|
||||
entry = coordinator.config_entry
|
||||
|
||||
data = coordinator.data[device_id]
|
||||
# Link configuration-URL for the gateway device
|
||||
configuration_url = (
|
||||
f"http://{entry.data[CONF_HOST]}"
|
||||
if device_id == gateway_id and entry
|
||||
else None
|
||||
)
|
||||
|
||||
# Build connections set
|
||||
connections = set()
|
||||
if mac := data.get("mac_address"):
|
||||
if mac := self.device.get("mac_address"):
|
||||
connections.add((CONNECTION_NETWORK_MAC, mac))
|
||||
if mac := data.get("zigbee_mac_address"):
|
||||
connections.add((CONNECTION_ZIGBEE, mac))
|
||||
if zigbee_mac := self.device.get("zigbee_mac_address"):
|
||||
connections.add((CONNECTION_ZIGBEE, zigbee_mac))
|
||||
|
||||
# Set base device info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=configuration_url,
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
connections=connections,
|
||||
manufacturer=data.get("vendor"),
|
||||
model=data.get("model"),
|
||||
model_id=data.get("model_id"),
|
||||
name=coordinator.api.smile.name,
|
||||
sw_version=data.get("firmware"),
|
||||
hw_version=data.get("hardware"),
|
||||
manufacturer=self.device.get("vendor"),
|
||||
model=self.device.get("model"),
|
||||
model_id=self.device.get("model_id"),
|
||||
name=api.smile.name,
|
||||
sw_version=self.device.get("firmware"),
|
||||
hw_version=self.device.get("hardware"),
|
||||
)
|
||||
|
||||
if device_id != coordinator.api.gateway_id:
|
||||
# Add extra info if not the gateway device
|
||||
if device_id != gateway_id:
|
||||
self._attr_device_info.update(
|
||||
{
|
||||
ATTR_NAME: data.get(ATTR_NAME),
|
||||
ATTR_VIA_DEVICE: (
|
||||
DOMAIN,
|
||||
str(self.coordinator.api.gateway_id),
|
||||
),
|
||||
ATTR_NAME: self.device.get(ATTR_NAME),
|
||||
ATTR_VIA_DEVICE: (DOMAIN, gateway_id),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ SELECT_TYPES = (
|
||||
PlugwiseSelectEntityDescription(
|
||||
key=SELECT_SCHEDULE,
|
||||
translation_key=SELECT_SCHEDULE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
options_key="available_schedules",
|
||||
),
|
||||
PlugwiseSelectEntityDescription(
|
||||
|
||||
@@ -43,6 +43,12 @@ from .models import DeviceState
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
# Roborock devices have a known issue where they go offline for a short period
|
||||
# around 3AM local time for ~1 minute and reset both the local connection
|
||||
# and MQTT connection. To avoid log spam, we will avoid reporting failures refreshing
|
||||
# data until this duration has passed.
|
||||
MIN_UNAVAILABLE_DURATION = timedelta(minutes=2)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -102,6 +108,9 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
# Keep track of last attempt to refresh maps/rooms to know when to try again.
|
||||
self._last_home_update_attempt: datetime
|
||||
self.last_home_update: datetime | None = None
|
||||
# Tracks the last successful update to control when we report failure
|
||||
# to the base class. This is reset on successful data update.
|
||||
self._last_update_success_time: datetime | None = None
|
||||
|
||||
@cached_property
|
||||
def dock_device_info(self) -> DeviceInfo:
|
||||
@@ -169,7 +178,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
self.last_home_update = dt_util.utcnow()
|
||||
|
||||
async def _verify_api(self) -> None:
|
||||
"""Verify that the api is reachable. If it is not, switch clients."""
|
||||
"""Verify that the api is reachable."""
|
||||
if self._device.is_connected:
|
||||
if self._device.is_local_connected:
|
||||
async_delete_issue(
|
||||
@@ -217,26 +226,27 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
try:
|
||||
# Update device props and standard api information
|
||||
await self._update_device_prop()
|
||||
except UpdateFailed:
|
||||
if self._should_suppress_update_failure():
|
||||
_LOGGER.debug(
|
||||
"Suppressing update failure until unavailable duration passed"
|
||||
)
|
||||
return self.data
|
||||
raise
|
||||
|
||||
# If the vacuum is currently cleaning and it has been IMAGE_CACHE_INTERVAL
|
||||
# since the last map update, you can update the map.
|
||||
new_status = self.properties_api.status
|
||||
if (
|
||||
new_status.in_cleaning
|
||||
and (dt_util.utcnow() - self._last_home_update_attempt)
|
||||
> IMAGE_CACHE_INTERVAL
|
||||
) or self.last_update_state != new_status.state_name:
|
||||
self._last_home_update_attempt = dt_util.utcnow()
|
||||
try:
|
||||
await self.update_map()
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.debug("Failed to update map: %s", err)
|
||||
except RoborockException as ex:
|
||||
_LOGGER.debug("Failed to update data: %s", ex)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_data_fail",
|
||||
) from ex
|
||||
# If the vacuum is currently cleaning and it has been IMAGE_CACHE_INTERVAL
|
||||
# since the last map update, you can update the map.
|
||||
new_status = self.properties_api.status
|
||||
if (
|
||||
new_status.in_cleaning
|
||||
and (dt_util.utcnow() - self._last_home_update_attempt)
|
||||
> IMAGE_CACHE_INTERVAL
|
||||
) or self.last_update_state != new_status.state_name:
|
||||
self._last_home_update_attempt = dt_util.utcnow()
|
||||
try:
|
||||
await self.update_map()
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.debug("Failed to update map: %s", err)
|
||||
|
||||
if self.properties_api.status.in_cleaning:
|
||||
if self._device.is_local_connected:
|
||||
@@ -248,6 +258,8 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
else:
|
||||
self.update_interval = V1_CLOUD_NOT_CLEANING_INTERVAL
|
||||
self.last_update_state = self.properties_api.status.state_name
|
||||
self._last_update_success_time = dt_util.utcnow()
|
||||
_LOGGER.debug("Data update successful %s", self._last_update_success_time)
|
||||
return DeviceState(
|
||||
status=self.properties_api.status,
|
||||
dnd_timer=self.properties_api.dnd,
|
||||
@@ -255,6 +267,23 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
clean_summary=self.properties_api.clean_summary,
|
||||
)
|
||||
|
||||
def _should_suppress_update_failure(self) -> bool:
|
||||
"""Determine if we should suppress update failure reporting.
|
||||
|
||||
We suppress reporting update failures until a minimum duration has
|
||||
passed since the last successful update. This is used to avoid reporting
|
||||
the device as unavailable for short periods, a known issue.
|
||||
|
||||
The intent is to apply to routine background state refreshes and not
|
||||
other failures such as the first update or map updates.
|
||||
"""
|
||||
if self._last_update_success_time is None:
|
||||
# Never had a successful update, do not suppress
|
||||
return False
|
||||
failure_duration = dt_util.utcnow() - self._last_update_success_time
|
||||
_LOGGER.debug("Update failure duration: %s", failure_duration)
|
||||
return failure_duration < MIN_UNAVAILABLE_DURATION
|
||||
|
||||
async def get_routines(self) -> list[HomeDataScene]:
|
||||
"""Get routines."""
|
||||
try:
|
||||
|
||||
@@ -416,8 +416,8 @@ def warn_dip(
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Entity %s %shas state class total_increasing, but its state is not"
|
||||
" strictly increasing. Triggered by state %s (%s) with last_updated set"
|
||||
" to %s. Please %s"
|
||||
" strictly increasing. Triggered by state %s (previous state: %s) with"
|
||||
" last_updated set to %s. Please %s"
|
||||
),
|
||||
entity_id,
|
||||
f"from integration {domain} " if domain else "",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.2.5"]
|
||||
"requirements": ["tesla-fleet-api==1.2.7"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.2.5", "teslemetry-stream==0.7.10"]
|
||||
"requirements": ["tesla-fleet-api==1.2.7", "teslemetry-stream==0.7.10"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.5"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.7"]
|
||||
}
|
||||
|
||||
@@ -84,12 +84,6 @@ from .exceptions import (
|
||||
ServiceValidationError,
|
||||
Unauthorized,
|
||||
)
|
||||
from .helpers.deprecation import (
|
||||
DeferredDeprecatedAlias,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from .helpers.json import json_bytes, json_fragment
|
||||
from .helpers.typing import VolSchemaType
|
||||
from .util import dt as dt_util
|
||||
@@ -161,18 +155,6 @@ class EventStateReportedData(EventStateEventData):
|
||||
old_last_reported: datetime.datetime
|
||||
|
||||
|
||||
def _deprecated_core_config() -> Any:
|
||||
from . import core_config # noqa: PLC0415
|
||||
|
||||
return core_config.Config
|
||||
|
||||
|
||||
# The Config class was moved to core_config in Home Assistant 2024.11
|
||||
_DEPRECATED_Config = DeferredDeprecatedAlias(
|
||||
_deprecated_core_config, "homeassistant.core_config.Config", "2025.11"
|
||||
)
|
||||
|
||||
|
||||
# How long to wait until things that run on startup have to finish.
|
||||
TIMEOUT_EVENT_START = 15
|
||||
|
||||
@@ -280,6 +262,8 @@ def async_get_hass_or_none() -> HomeAssistant | None:
|
||||
|
||||
|
||||
class ReleaseChannel(enum.StrEnum):
|
||||
"""Release channel."""
|
||||
|
||||
BETA = "beta"
|
||||
DEV = "dev"
|
||||
NIGHTLY = "nightly"
|
||||
@@ -2883,11 +2867,3 @@ class ServiceRegistry:
|
||||
if TYPE_CHECKING:
|
||||
target = cast(Callable[..., ServiceResponse], target)
|
||||
return await self._hass.async_add_executor_job(target, service_call)
|
||||
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = functools.partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = functools.partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
},
|
||||
"aemet": {
|
||||
"name": "AEMET OpenData",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -95,7 +95,7 @@
|
||||
},
|
||||
"aftership": {
|
||||
"name": "AfterShip",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -119,7 +119,7 @@
|
||||
},
|
||||
"airnow": {
|
||||
"name": "AirNow",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -151,7 +151,7 @@
|
||||
"name": "Airthings"
|
||||
},
|
||||
"airthings_ble": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Airthings BLE"
|
||||
@@ -160,7 +160,7 @@
|
||||
},
|
||||
"airtouch4": {
|
||||
"name": "AirTouch 4",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -276,7 +276,7 @@
|
||||
},
|
||||
"amberelectric": {
|
||||
"name": "Amber Electric",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -323,7 +323,7 @@
|
||||
},
|
||||
"android_ip_webcam": {
|
||||
"name": "Android IP Webcam",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -359,7 +359,7 @@
|
||||
},
|
||||
"anthemav": {
|
||||
"name": "Anthem A/V Receivers",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -505,7 +505,7 @@
|
||||
},
|
||||
"arcam_fmj": {
|
||||
"name": "Arcam FMJ Receivers",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -569,7 +569,7 @@
|
||||
},
|
||||
"atag": {
|
||||
"name": "Atag",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -608,7 +608,7 @@
|
||||
}
|
||||
},
|
||||
"aurora": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -620,7 +620,7 @@
|
||||
},
|
||||
"aussie_broadband": {
|
||||
"name": "Aussie Broadband",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -663,7 +663,7 @@
|
||||
},
|
||||
"baf": {
|
||||
"name": "Big Ass Fans",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -680,7 +680,7 @@
|
||||
},
|
||||
"balboa": {
|
||||
"name": "Balboa Spa Client",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -738,7 +738,7 @@
|
||||
},
|
||||
"blebox": {
|
||||
"name": "BleBox devices",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -778,13 +778,13 @@
|
||||
},
|
||||
"bluemaestro": {
|
||||
"name": "BlueMaestro",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"bluesound": {
|
||||
"name": "Bluesound",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
@@ -865,7 +865,7 @@
|
||||
},
|
||||
"brottsplatskartan": {
|
||||
"name": "Brottsplatskartan",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -928,7 +928,7 @@
|
||||
},
|
||||
"buienradar": {
|
||||
"name": "Buienradar",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -939,7 +939,7 @@
|
||||
},
|
||||
"caldav": {
|
||||
"name": "CalDAV",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -963,7 +963,7 @@
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"cert_expiry": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -1039,7 +1039,7 @@
|
||||
},
|
||||
"cloudflare": {
|
||||
"name": "Cloudflare",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push",
|
||||
"single_config_entry": true
|
||||
@@ -1063,7 +1063,7 @@
|
||||
},
|
||||
"coinbase": {
|
||||
"name": "Coinbase",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -1395,7 +1395,7 @@
|
||||
},
|
||||
"dnsip": {
|
||||
"name": "DNS IP",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
@@ -1407,7 +1407,7 @@
|
||||
},
|
||||
"doorbird": {
|
||||
"name": "DoorBird",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
@@ -3982,19 +3982,19 @@
|
||||
"name": "Microsoft",
|
||||
"integrations": {
|
||||
"azure_data_explorer": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Azure Data Explorer"
|
||||
},
|
||||
"azure_devops": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Azure DevOps"
|
||||
},
|
||||
"azure_event_hub": {
|
||||
"integration_type": "hub",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Azure Event Hub"
|
||||
|
||||
8
requirements_all.txt
generated
8
requirements_all.txt
generated
@@ -206,7 +206,7 @@ aioaquacell==0.2.0
|
||||
aioaseko==1.0.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.5.2
|
||||
aioasuswrt==1.5.3
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.7.1
|
||||
@@ -319,7 +319,7 @@ aiolookin==1.0.0
|
||||
aiolyric==2.0.2
|
||||
|
||||
# homeassistant.components.mealie
|
||||
aiomealie==1.1.0
|
||||
aiomealie==1.1.1
|
||||
|
||||
# homeassistant.components.modern_forms
|
||||
aiomodernforms==0.1.8
|
||||
@@ -2560,7 +2560,7 @@ python-opensky==1.0.1
|
||||
python-otbr-api==2.7.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.7.1
|
||||
python-overseerr==0.8.0
|
||||
|
||||
# homeassistant.components.picnic
|
||||
python-picnic-api2==1.3.1
|
||||
@@ -2978,7 +2978,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.2.5
|
||||
tesla-fleet-api==1.2.7
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
||||
8
requirements_test_all.txt
generated
8
requirements_test_all.txt
generated
@@ -197,7 +197,7 @@ aioaquacell==0.2.0
|
||||
aioaseko==1.0.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.5.2
|
||||
aioasuswrt==1.5.3
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2.7.1
|
||||
@@ -304,7 +304,7 @@ aiolookin==1.0.0
|
||||
aiolyric==2.0.2
|
||||
|
||||
# homeassistant.components.mealie
|
||||
aiomealie==1.1.0
|
||||
aiomealie==1.1.1
|
||||
|
||||
# homeassistant.components.modern_forms
|
||||
aiomodernforms==0.1.8
|
||||
@@ -2144,7 +2144,7 @@ python-opensky==1.0.1
|
||||
python-otbr-api==2.7.0
|
||||
|
||||
# homeassistant.components.overseerr
|
||||
python-overseerr==0.7.1
|
||||
python-overseerr==0.8.0
|
||||
|
||||
# homeassistant.components.picnic
|
||||
python-picnic-api2==1.3.1
|
||||
@@ -2478,7 +2478,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.2.5
|
||||
tesla-fleet-api==1.2.7
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
||||
@@ -55,6 +55,55 @@
|
||||
'state': '22.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_thermostat_device_uptime-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.test_thermostat_device_uptime',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Device uptime',
|
||||
'platform': 'airobot',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'device_uptime',
|
||||
'unique_id': 'T01A1B2C3_device_uptime',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_thermostat_device_uptime-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
'friendly_name': 'Test Thermostat Device uptime',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_thermostat_device_uptime',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2023-12-31T21:13:20+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_thermostat_error_count-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
||||
@@ -16,6 +16,7 @@ def platforms() -> list[Platform]:
|
||||
return [Platform.SENSOR]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2024-01-01 00:00:00+00:00")
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
"code": "CMBrewByWeightDoses",
|
||||
"index": 1,
|
||||
"output": {
|
||||
"scaleConnected": false,
|
||||
"scaleConnected": true,
|
||||
"availableModes": ["Continuous"],
|
||||
"mode": "Continuous",
|
||||
"doses": {
|
||||
|
||||
@@ -1,4 +1,122 @@
|
||||
# serializer version: 1
|
||||
# name: test_brew_by_weight_dose[Linea Mini][entry-dose-1]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 100,
|
||||
'min': 5,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.lm012345_brew_by_weight_dose_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.WEIGHT: 'weight'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Brew by weight Dose 1',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bbw_dose',
|
||||
'unique_id': 'LM012345_bbw_dose_1',
|
||||
'unit_of_measurement': <UnitOfMass.GRAMS: 'g'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_brew_by_weight_dose[Linea Mini][entry-dose-2]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 100,
|
||||
'min': 5,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.lm012345_brew_by_weight_dose_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <NumberDeviceClass.WEIGHT: 'weight'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Brew by weight Dose 2',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bbw_dose',
|
||||
'unique_id': 'LM012345_bbw_dose_2',
|
||||
'unit_of_measurement': <UnitOfMass.GRAMS: 'g'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_brew_by_weight_dose[Linea Mini][state-dose-1]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'weight',
|
||||
'friendly_name': 'LM012345 Brew by weight Dose 1',
|
||||
'max': 100,
|
||||
'min': 5,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
'unit_of_measurement': <UnitOfMass.GRAMS: 'g'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.lm012345_brew_by_weight_dose_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '34.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_brew_by_weight_dose[Linea Mini][state-dose-2]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'weight',
|
||||
'friendly_name': 'LM012345 Brew by weight Dose 2',
|
||||
'max': 100,
|
||||
'min': 5,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
'unit_of_measurement': <UnitOfMass.GRAMS: 'g'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.lm012345_brew_by_weight_dose_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '17.5',
|
||||
})
|
||||
# ---
|
||||
# name: test_general_numbers[coffee_target_temperature-94-set_coffee_target_temperature-kwargs0]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
||||
@@ -1,4 +1,63 @@
|
||||
# serializer version: 1
|
||||
# name: test_bbw_dose_mode[Linea Mini]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'LM012345 Brew by weight dose mode',
|
||||
'options': list([
|
||||
'continuous',
|
||||
'dose1',
|
||||
'dose2',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'select.lm012345_brew_by_weight_dose_mode',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'continuous',
|
||||
})
|
||||
# ---
|
||||
# name: test_bbw_dose_mode[Linea Mini].1
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'continuous',
|
||||
'dose1',
|
||||
'dose2',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'select.lm012345_brew_by_weight_dose_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Brew by weight dose mode',
|
||||
'platform': 'lamarzocco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'bbw_dose_mode',
|
||||
'unique_id': 'LM012345_bbw_dose_mode',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_pre_brew_infusion_select[GS3 AV]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pylamarzocco.const import (
|
||||
DoseMode,
|
||||
ModelName,
|
||||
PreExtractionMode,
|
||||
SmartStandByType,
|
||||
@@ -27,6 +28,11 @@ from . import async_init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
DOSE_MODE_HA_TO_LM = {
|
||||
"dose1": DoseMode.DOSE_1,
|
||||
"dose2": DoseMode.DOSE_2,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_name", "value", "func_name", "kwargs"),
|
||||
@@ -291,3 +297,45 @@ async def test_steam_temperature(
|
||||
mock_lamarzocco.set_steam_target_temperature.assert_called_once_with(
|
||||
temperature=128.3,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MINI])
|
||||
async def test_brew_by_weight_dose(
|
||||
hass: HomeAssistant,
|
||||
mock_lamarzocco: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test brew by weight dose."""
|
||||
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
for dose in (1, 2):
|
||||
entity_id = f"number.{serial_number}_brew_by_weight_dose_{dose}"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
|
||||
assert state
|
||||
assert state == snapshot(name=f"state-dose-{dose}")
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
||||
assert entry.device_id
|
||||
assert entry == snapshot(name=f"entry-dose-{dose}")
|
||||
|
||||
# service call
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_VALUE: 42,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_lamarzocco.set_brew_by_weight_dose.assert_called_with(
|
||||
dose=DOSE_MODE_HA_TO_LM[f"dose{dose}"],
|
||||
value=42,
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pylamarzocco.const import (
|
||||
DoseMode,
|
||||
ModelName,
|
||||
PreExtractionMode,
|
||||
SmartStandByType,
|
||||
@@ -193,3 +194,40 @@ async def test_select_errors(
|
||||
blocking=True,
|
||||
)
|
||||
assert exc_info.value.translation_key == "select_option_error"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
@pytest.mark.parametrize("device_fixture", [ModelName.LINEA_MINI])
|
||||
async def test_bbw_dose_mode(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_lamarzocco: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the La Marzocco Brew By Weight Mode Select (only for Mini R Models)."""
|
||||
|
||||
serial_number = mock_lamarzocco.serial_number
|
||||
|
||||
state = hass.states.get(f"select.{serial_number}_brew_by_weight_dose_mode")
|
||||
|
||||
assert state
|
||||
assert state == snapshot
|
||||
|
||||
entry = entity_registry.async_get(state.entity_id)
|
||||
assert entry
|
||||
assert entry == snapshot
|
||||
|
||||
# on/off service calls
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: f"select.{serial_number}_brew_by_weight_dose_mode",
|
||||
ATTR_OPTION: "dose2",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_lamarzocco.set_brew_by_weight_dose_mode.assert_called_once_with(
|
||||
mode=DoseMode.DOSE_2
|
||||
)
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
{
|
||||
"date": "2024-01-21",
|
||||
"entryType": "lunch",
|
||||
"entryType": "dessert",
|
||||
"title": "",
|
||||
"text": "",
|
||||
"recipeId": "27455eb2-31d3-4682-84ff-02a114bf293a",
|
||||
@@ -178,7 +178,7 @@
|
||||
},
|
||||
{
|
||||
"date": "2024-01-21",
|
||||
"entryType": "dinner",
|
||||
"entryType": "snack",
|
||||
"title": "",
|
||||
"text": "",
|
||||
"recipeId": "48f39d27-4b8e-4c14-bf36-4e1e6497e75e",
|
||||
@@ -218,7 +218,7 @@
|
||||
},
|
||||
{
|
||||
"date": "2024-01-21",
|
||||
"entryType": "dinner",
|
||||
"entryType": "drink",
|
||||
"title": "",
|
||||
"text": "",
|
||||
"recipeId": "27455eb2-31d3-4682-84ff-02a114bf293a",
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
},
|
||||
{
|
||||
"date": "2024-01-23",
|
||||
"entryType": "dinner",
|
||||
"entryType": "dessert",
|
||||
"title": "",
|
||||
"text": "",
|
||||
"recipeId": "47595e4c-52bc-441d-b273-3edf4258806d",
|
||||
@@ -500,7 +500,7 @@
|
||||
},
|
||||
{
|
||||
"date": "2024-01-22",
|
||||
"entryType": "dinner",
|
||||
"entryType": "drink",
|
||||
"title": "",
|
||||
"text": "",
|
||||
"recipeId": "9d553779-607e-471b-acf3-84e6be27b159",
|
||||
@@ -574,7 +574,7 @@
|
||||
},
|
||||
{
|
||||
"date": "2024-01-22",
|
||||
"entryType": "dinner",
|
||||
"entryType": "snack",
|
||||
"title": "",
|
||||
"text": "",
|
||||
"recipeId": "55c88810-4cf1-4d86-ae50-63b15fd173fb",
|
||||
|
||||
@@ -49,20 +49,6 @@
|
||||
'summary': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο (1)',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!',
|
||||
'end': dict({
|
||||
'date': '2024-01-24',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'date': '2024-01-23',
|
||||
}),
|
||||
'summary': 'Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': 'This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.',
|
||||
'end': dict({
|
||||
@@ -105,20 +91,6 @@
|
||||
'summary': 'All-American Beef Stew Recipe',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!',
|
||||
'end': dict({
|
||||
'date': '2024-01-23',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'date': '2024-01-22',
|
||||
}),
|
||||
'summary': 'Einfacher Nudelauflauf mit Brokkoli',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': 'Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.',
|
||||
'end': dict({
|
||||
@@ -133,20 +105,6 @@
|
||||
'summary': 'Miso Udon Noodles with Spinach and Tofu',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !',
|
||||
'end': dict({
|
||||
'date': '2024-01-23',
|
||||
}),
|
||||
'location': None,
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'date': '2024-01-22',
|
||||
}),
|
||||
'summary': 'Mousse de saumon',
|
||||
'uid': None,
|
||||
}),
|
||||
dict({
|
||||
'description': 'Dineren met de boys',
|
||||
'end': dict({
|
||||
|
||||
@@ -36,6 +36,37 @@
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
]),
|
||||
'dessert': list([
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'dessert',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': dict({
|
||||
'__type': "<class 'datetime.date'>",
|
||||
'isoformat': '2024-01-23',
|
||||
}),
|
||||
'mealplan_id': 221,
|
||||
'recipe': dict({
|
||||
'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'image': 'Kn62',
|
||||
'name': 'Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce',
|
||||
'original_url': 'https://www.ambitiouskitchen.com/greek-turkey-meatballs/',
|
||||
'perform_time': '20 Minutes',
|
||||
'prep_time': '40 Minutes',
|
||||
'rating': None,
|
||||
'recipe_id': '47595e4c-52bc-441d-b273-3edf4258806d',
|
||||
'recipe_yield': '4 servings',
|
||||
'slug': 'greek-turkey-meatballs-with-lemon-orzo-creamy-feta-yogurt-sauce',
|
||||
'total_time': '1 Hour',
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
]),
|
||||
'dinner': list([
|
||||
dict({
|
||||
'description': None,
|
||||
@@ -95,35 +126,6 @@
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'dinner',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': dict({
|
||||
'__type': "<class 'datetime.date'>",
|
||||
'isoformat': '2024-01-23',
|
||||
}),
|
||||
'mealplan_id': 221,
|
||||
'recipe': dict({
|
||||
'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'image': 'Kn62',
|
||||
'name': 'Greek Turkey Meatballs with Lemon Orzo & Creamy Feta Yogurt Sauce',
|
||||
'original_url': 'https://www.ambitiouskitchen.com/greek-turkey-meatballs/',
|
||||
'perform_time': '20 Minutes',
|
||||
'prep_time': '40 Minutes',
|
||||
'rating': None,
|
||||
'recipe_id': '47595e4c-52bc-441d-b273-3edf4258806d',
|
||||
'recipe_yield': '4 servings',
|
||||
'slug': 'greek-turkey-meatballs-with-lemon-orzo-creamy-feta-yogurt-sauce',
|
||||
'total_time': '1 Hour',
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'dinner',
|
||||
@@ -211,35 +213,6 @@
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'dinner',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': dict({
|
||||
'__type': "<class 'datetime.date'>",
|
||||
'isoformat': '2024-01-22',
|
||||
}),
|
||||
'mealplan_id': 211,
|
||||
'recipe': dict({
|
||||
'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'image': 'nOPT',
|
||||
'name': 'Einfacher Nudelauflauf mit Brokkoli',
|
||||
'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/',
|
||||
'perform_time': '20 Minutes',
|
||||
'prep_time': '15 Minutes',
|
||||
'rating': None,
|
||||
'recipe_id': '9d553779-607e-471b-acf3-84e6be27b159',
|
||||
'recipe_yield': '4 servings',
|
||||
'slug': 'einfacher-nudelauflauf-mit-brokkoli',
|
||||
'total_time': '35 Minutes',
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'dinner',
|
||||
@@ -269,35 +242,6 @@
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'dinner',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': dict({
|
||||
'__type': "<class 'datetime.date'>",
|
||||
'isoformat': '2024-01-22',
|
||||
}),
|
||||
'mealplan_id': 195,
|
||||
'recipe': dict({
|
||||
'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'image': 'rrNL',
|
||||
'name': 'Mousse de saumon',
|
||||
'original_url': 'https://www.ricardocuisine.com/recettes/8919-mousse-de-saumon',
|
||||
'perform_time': '2 Minutes',
|
||||
'prep_time': '15 Minutes',
|
||||
'rating': None,
|
||||
'recipe_id': '55c88810-4cf1-4d86-ae50-63b15fd173fb',
|
||||
'recipe_yield': '12 servings',
|
||||
'slug': 'mousse-de-saumon',
|
||||
'total_time': '17 Minutes',
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
dict({
|
||||
'description': 'Dineren met de boys',
|
||||
'entry_type': 'dinner',
|
||||
@@ -313,6 +257,37 @@
|
||||
'user_id': '6caa6e4d-521f-4ef4-9ed7-388bdd63f47d',
|
||||
}),
|
||||
]),
|
||||
'drink': list([
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'drink',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': dict({
|
||||
'__type': "<class 'datetime.date'>",
|
||||
'isoformat': '2024-01-22',
|
||||
}),
|
||||
'mealplan_id': 211,
|
||||
'recipe': dict({
|
||||
'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'image': 'nOPT',
|
||||
'name': 'Einfacher Nudelauflauf mit Brokkoli',
|
||||
'original_url': 'https://kochkarussell.com/einfacher-nudelauflauf-brokkoli/',
|
||||
'perform_time': '20 Minutes',
|
||||
'prep_time': '15 Minutes',
|
||||
'rating': None,
|
||||
'recipe_id': '9d553779-607e-471b-acf3-84e6be27b159',
|
||||
'recipe_yield': '4 servings',
|
||||
'slug': 'einfacher-nudelauflauf-mit-brokkoli',
|
||||
'total_time': '35 Minutes',
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
]),
|
||||
'lunch': list([
|
||||
dict({
|
||||
'description': None,
|
||||
@@ -433,6 +408,37 @@
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
]),
|
||||
'snack': list([
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': 'snack',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': dict({
|
||||
'__type': "<class 'datetime.date'>",
|
||||
'isoformat': '2024-01-22',
|
||||
}),
|
||||
'mealplan_id': 195,
|
||||
'recipe': dict({
|
||||
'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !',
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'image': 'rrNL',
|
||||
'name': 'Mousse de saumon',
|
||||
'original_url': 'https://www.ricardocuisine.com/recettes/8919-mousse-de-saumon',
|
||||
'perform_time': '2 Minutes',
|
||||
'prep_time': '15 Minutes',
|
||||
'rating': None,
|
||||
'recipe_id': '55c88810-4cf1-4d86-ae50-63b15fd173fb',
|
||||
'recipe_yield': '12 servings',
|
||||
'slug': 'mousse-de-saumon',
|
||||
'total_time': '17 Minutes',
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
'title': None,
|
||||
'user_id': '1ce8b5fe-04e8-4b80-aab1-d92c94685c6d',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
'shoppinglist': dict({
|
||||
'27edbaab-2ec6-441f-8490-0283ea77585f': dict({
|
||||
|
||||
@@ -1967,7 +1967,7 @@
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': <MealplanEntryType.DINNER: 'dinner'>,
|
||||
'entry_type': <MealplanEntryType.DESSERT: 'dessert'>,
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': HAFakeDate(2024, 1, 23),
|
||||
@@ -2123,7 +2123,7 @@
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': <MealplanEntryType.DINNER: 'dinner'>,
|
||||
'entry_type': <MealplanEntryType.DRINK: 'drink'>,
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': HAFakeDate(2024, 1, 22),
|
||||
@@ -2175,7 +2175,7 @@
|
||||
}),
|
||||
dict({
|
||||
'description': None,
|
||||
'entry_type': <MealplanEntryType.DINNER: 'dinner'>,
|
||||
'entry_type': <MealplanEntryType.SNACK: 'snack'>,
|
||||
'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157',
|
||||
'household_id': None,
|
||||
'mealplan_date': HAFakeDate(2024, 1, 22),
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.bathroom_thermostat_schedule',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -263,7 +263,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.living_room_thermostat_schedule',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -386,7 +386,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.badkamer_thermostat_schedule',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -451,7 +451,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.bios_thermostat_schedule',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -516,7 +516,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.jessie_thermostat_schedule',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -581,7 +581,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.woonkamer_thermostat_schedule',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Test for Roborock init."""
|
||||
|
||||
import datetime
|
||||
import pathlib
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from roborock import (
|
||||
RoborockInvalidCredentials,
|
||||
@@ -12,9 +14,13 @@ from roborock import (
|
||||
)
|
||||
from roborock.exceptions import RoborockException
|
||||
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
)
|
||||
from homeassistant.components.roborock.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||
@@ -23,7 +29,7 @@ from homeassistant.setup import async_setup_component
|
||||
from .conftest import FakeDevice
|
||||
from .mock_data import ROBOROCK_RRUID, USER_EMAIL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@@ -295,6 +301,72 @@ async def test_migrate_config_entry_unique_id(
|
||||
assert config_entry.unique_id == ROBOROCK_RRUID
|
||||
|
||||
|
||||
@pytest.mark.parametrize("platforms", [[Platform.SENSOR]])
|
||||
async def test_update_unavailability_threshold(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
setup_entry: MockConfigEntry,
|
||||
fake_vacuum: FakeDevice,
|
||||
) -> None:
|
||||
"""Test that a small number of update failures are suppressed before marking a device unavailable."""
|
||||
await async_setup_component(hass, HA_DOMAIN, {})
|
||||
assert setup_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# We pick an arbitrary sensor to test for availability
|
||||
sensor_entity_id = "sensor.roborock_s7_maxv_battery"
|
||||
expected_state = "100"
|
||||
state = hass.states.get(sensor_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
|
||||
# Simulate a few update failures below the threshold
|
||||
assert fake_vacuum.v1_properties is not None
|
||||
fake_vacuum.v1_properties.status.refresh.side_effect = RoborockException(
|
||||
"Simulated update failure"
|
||||
)
|
||||
|
||||
# Move forward in time less than the threshold
|
||||
freezer.tick(datetime.timedelta(seconds=90))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Force a coordinator refresh.
|
||||
await hass.services.async_call(
|
||||
HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
{ATTR_ENTITY_ID: sensor_entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify that the entity is still available
|
||||
state = hass.states.get(sensor_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
|
||||
# Move forward in time to exceed the threshold
|
||||
freezer.tick(datetime.timedelta(minutes=3))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify that the entity is now unavailable
|
||||
state = hass.states.get(sensor_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == "unavailable"
|
||||
|
||||
# Now restore normal update behavior and refresh.
|
||||
fake_vacuum.v1_properties.status.refresh.side_effect = None
|
||||
|
||||
freezer.tick(datetime.timedelta(seconds=45))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify that the entity recovers and is available again
|
||||
state = hass.states.get(sensor_entity_id)
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
|
||||
|
||||
async def test_cloud_api_repair(
|
||||
hass: HomeAssistant,
|
||||
mock_roborock_entry: MockConfigEntry,
|
||||
|
||||
@@ -2261,8 +2261,8 @@ async def test_compile_hourly_sum_statistics_total_increasing_small_dip(
|
||||
last_updated = states["sensor.test1"][6].last_updated.isoformat()
|
||||
assert (
|
||||
"Entity sensor.test1 has state class total_increasing, but its state is not "
|
||||
f"strictly increasing. Triggered by state {state} ({previous_state}) with "
|
||||
f"last_updated set to {last_updated}. Please create a bug report at "
|
||||
f"strictly increasing. Triggered by state {state} (previous state: {previous_state}) "
|
||||
f"with last_updated set to {last_updated}. Please create a bug report at "
|
||||
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue"
|
||||
) in caplog.text
|
||||
statistic_ids = await async_list_statistic_ids(hass)
|
||||
|
||||
@@ -49,7 +49,6 @@ from homeassistant.core import (
|
||||
callback,
|
||||
get_release_channel,
|
||||
)
|
||||
from homeassistant.core_config import Config
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
InvalidEntityFormatError,
|
||||
@@ -64,12 +63,7 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.read_only_dict import ReadOnlyDict
|
||||
|
||||
from .common import (
|
||||
async_capture_events,
|
||||
async_mock_service,
|
||||
help_test_all,
|
||||
import_and_test_deprecated_alias,
|
||||
)
|
||||
from .common import async_capture_events, async_mock_service
|
||||
|
||||
PST = dt_util.get_time_zone("America/Los_Angeles")
|
||||
|
||||
@@ -3019,16 +3013,6 @@ async def test_cancel_shutdown_job(hass: HomeAssistant) -> None:
|
||||
assert not evt.is_set()
|
||||
|
||||
|
||||
def test_all() -> None:
|
||||
"""Test module.__all__ is correctly set."""
|
||||
help_test_all(ha)
|
||||
|
||||
|
||||
def test_deprecated_config(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test deprecated Config class."""
|
||||
import_and_test_deprecated_alias(caplog, ha, "Config", Config, "2025.11")
|
||||
|
||||
|
||||
def test_one_time_listener_repr(hass: HomeAssistant) -> None:
|
||||
"""Test one time listener repr."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user