Compare commits

..

2 Commits

Author SHA1 Message Date
Michael
2af60d67be Apply suggestions from code review
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 22:54:03 +01:00
mib1185
8b2d7b1ae9 add turned_off and turned_on triggers 2025-12-12 21:34:21 +00:00
113 changed files with 659 additions and 1104 deletions

8
CODEOWNERS generated
View File

@@ -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
/tests/components/blink/ @fronzbot
/homeassistant/components/blink/ @fronzbot @mkmer
/tests/components/blink/ @fronzbot @mkmer
/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 @davidrecordon
/tests/components/control4/ @lawtancool @davidrecordon
/homeassistant/components/control4/ @lawtancool
/tests/components/control4/ @lawtancool
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
/homeassistant/components/cookidoo/ @miaucl

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aftership",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["pyaftership==21.11.0"]
}

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from pyairobotrest.models import ThermostatStatus
@@ -24,8 +23,6 @@ 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
@@ -37,15 +34,10 @@ PARALLEL_UPDATES = 0
class AirobotSensorEntityDescription(SensorEntityDescription):
"""Describes Airobot sensor entity."""
value_fn: Callable[[ThermostatStatus], StateType | datetime]
value_fn: Callable[[ThermostatStatus], StateType]
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",
@@ -104,14 +96,6 @@ 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,
),
)
@@ -145,6 +129,6 @@ class AirobotSensor(AirobotEntity, SensorEntity):
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
@property
def native_value(self) -> StateType | datetime:
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data.status)

View File

@@ -17,7 +17,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/airthings",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["airthings"],
"requirements": ["airthings-cloud==0.2.0"]

View File

@@ -27,7 +27,6 @@
"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"]
}

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -9,7 +9,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/airzone",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.4"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]
}

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]
}

View File

@@ -4,7 +4,6 @@
"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"],

View File

@@ -4,7 +4,6 @@
"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"]
}

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.5.3", "asusrouter==1.21.3"]
"requirements": ["aioasuswrt==1.5.2", "asusrouter==1.21.3"]
}

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -27,7 +27,6 @@
}
],
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -131,6 +131,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"lawn_mower",
"light",
"media_player",
"siren",
"switch",
"text",
"vacuum",

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"],

View File

@@ -4,7 +4,6 @@
"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": [

View File

@@ -12,7 +12,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/balboa",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pybalboa"],
"requirements": ["pybalboa==1.1.3"]

View File

@@ -4,7 +4,6 @@
"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"],

View File

@@ -1,7 +1,7 @@
{
"domain": "blink",
"name": "Blink",
"codeowners": ["@fronzbot"],
"codeowners": ["@fronzbot", "@mkmer"],
"config_flow": true,
"dhcp": [
{
@@ -18,7 +18,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/blink",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.25.1"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -11,7 +11,6 @@
"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"]
}

View File

@@ -5,7 +5,6 @@
"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": [

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -14,7 +14,6 @@
}
],
"documentation": "https://www.home-assistant.io/integrations/bond",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["bond_async"],
"requirements": ["bond-async==0.2.1"],

View File

@@ -5,7 +5,6 @@
"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"],

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -5,7 +5,6 @@
"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"],

View File

@@ -4,7 +4,6 @@
"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"]
}

View File

@@ -4,6 +4,5 @@
"codeowners": ["@jjlawren"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cert_expiry",
"integration_type": "service",
"iot_class": "cloud_polling"
}

View File

@@ -4,7 +4,6 @@
"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"],

View File

@@ -4,7 +4,6 @@
"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"]

View File

@@ -1,10 +1,9 @@
{
"domain": "control4",
"name": "Control4",
"codeowners": ["@lawtancool", "@davidrecordon"],
"codeowners": ["@lawtancool"],
"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"],

View File

@@ -4,7 +4,6 @@
"codeowners": ["@fredrike"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/daikin",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.17.1"],

View File

@@ -4,7 +4,6 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/datadog",
"integration_type": "service",
"iot_class": "local_push",
"loggers": ["datadog"],
"quality_scale": "legacy",

View File

@@ -4,7 +4,6 @@
"codeowners": ["@gagebenne"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dexcom",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pydexcom"],
"requirements": ["pydexcom==0.2.3"]

View File

@@ -4,7 +4,6 @@
"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"]
}

View File

@@ -5,7 +5,6 @@
"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"],

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"dependencies": ["mqtt"],
"documentation": "https://www.home-assistant.io/integrations/drop_connect",
"integration_type": "hub",
"iot_class": "local_push",
"mqtt": ["drop_connect/discovery/#"],
"requirements": ["dropmqttapi==1.0.3"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@sarahseidman"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/droplet",
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["pydroplet==2.3.4"],

View File

@@ -4,7 +4,6 @@
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dunehd",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pdunehd"],
"requirements": ["pdunehd==1.3.2"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@cereal2nd"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/duotecno",
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"],
"requirements": ["pyDuotecno==2024.10.1"],

View File

@@ -4,7 +4,6 @@
"codeowners": ["@Jc2k"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/eafm",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aioeafm"],
"requirements": ["aioeafm==0.1.2"]

View File

@@ -7,7 +7,6 @@
"homekit": {
"models": ["EB", "ecobee*"]
},
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyecobee"],
"requirements": ["python-ecobee-api==0.3.2"],

View File

@@ -4,7 +4,6 @@
"codeowners": ["@pjanuario"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecoforest",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyecoforest"],
"requirements": ["pyecoforest==0.4.0"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@w1ll1am23"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/econet",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["paho_mqtt", "pyeconet"],
"requirements": ["pyeconet==0.1.28"]

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"dependencies": ["application_credentials", "http"],
"documentation": "https://www.home-assistant.io/integrations/ekeybionyx",
"integration_type": "hub",
"iot_class": "local_push",
"quality_scale": "bronze",
"requirements": ["ekey-bionyxpy==1.0.0"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@jafar-atili"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/electrasmart",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["pyElectra==1.2.4"]
}

View File

@@ -42,11 +42,11 @@
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"api_key": "The API key for authenticating with Firefly III",
"api_key": "The API key for authenticating with Firefly",
"url": "[%key:common::config_flow::data::url%]",
"verify_ssl": "Verify the SSL certificate of the Firefly III instance"
"verify_ssl": "Verify the SSL certificate of the Firefly instance"
},
"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)."
"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)."
}
}
},
@@ -84,13 +84,13 @@
},
"exceptions": {
"cannot_connect": {
"message": "An error occurred while trying to connect to the Firefly III instance: {error}"
"message": "An error occurred while trying to connect to the Firefly 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 III instance: {error}"
"message": "A timeout occurred while trying to connect to the Firefly instance: {error}"
}
}
}

View File

@@ -6,7 +6,7 @@ from dataclasses import dataclass
from datetime import timedelta
from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
from pyfritzhome.devicetypes import FritzhomeTemplate, FritzhomeTrigger
from pyfritzhome.devicetypes import FritzhomeTemplate
from requests.exceptions import ConnectionError as RequestConnectionError, HTTPError
from homeassistant.config_entries import ConfigEntry
@@ -27,7 +27,6 @@ class FritzboxCoordinatorData:
devices: dict[str, FritzhomeDevice]
templates: dict[str, FritzhomeTemplate]
triggers: dict[str, FritzhomeTrigger]
supported_color_properties: dict[str, tuple[dict, list]]
@@ -38,7 +37,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
configuration_url: str
fritz: Fritzhome
has_templates: bool
has_triggers: bool
def __init__(self, hass: HomeAssistant, config_entry: FritzboxConfigEntry) -> None:
"""Initialize the Fritzbox Smarthome device coordinator."""
@@ -52,9 +50,8 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
self.new_devices: set[str] = set()
self.new_templates: set[str] = set()
self.new_triggers: set[str] = set()
self.data = FritzboxCoordinatorData({}, {}, {}, {})
self.data = FritzboxCoordinatorData({}, {}, {})
async def async_setup(self) -> None:
"""Set up the coordinator."""
@@ -77,11 +74,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
)
LOGGER.debug("enable smarthome templates: %s", self.has_templates)
self.has_triggers = await self.hass.async_add_executor_job(
self.fritz.has_triggers
)
LOGGER.debug("enable smarthome triggers: %s", self.has_triggers)
self.configuration_url = self.fritz.get_prefixed_host()
await self.async_config_entry_first_refresh()
@@ -100,7 +92,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
available_main_ains = [
ain
for ain, dev in (data.devices | data.templates | data.triggers).items()
for ain, dev in data.devices.items() | data.templates.items()
if dev.device_and_unit_id[1] is None
]
device_reg = dr.async_get(self.hass)
@@ -120,9 +112,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
self.fritz.update_devices(ignore_removed=False)
if self.has_templates:
self.fritz.update_templates(ignore_removed=False)
if self.has_triggers:
self.fritz.update_triggers(ignore_removed=False)
except RequestConnectionError as ex:
raise UpdateFailed from ex
except HTTPError:
@@ -134,8 +123,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
self.fritz.update_devices(ignore_removed=False)
if self.has_templates:
self.fritz.update_templates(ignore_removed=False)
if self.has_triggers:
self.fritz.update_triggers(ignore_removed=False)
devices = self.fritz.get_devices()
device_data = {}
@@ -169,20 +156,12 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
for template in templates:
template_data[template.ain] = template
trigger_data = {}
if self.has_triggers:
triggers = self.fritz.get_triggers()
for trigger in triggers:
trigger_data[trigger.ain] = trigger
self.new_devices = device_data.keys() - self.data.devices.keys()
self.new_templates = template_data.keys() - self.data.templates.keys()
self.new_triggers = trigger_data.keys() - self.data.triggers.keys()
return FritzboxCoordinatorData(
devices=device_data,
templates=template_data,
triggers=trigger_data,
supported_color_properties=supported_color_properties,
)
@@ -214,7 +193,6 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
if (
self.data.devices.keys() - new_data.devices.keys()
or self.data.templates.keys() - new_data.templates.keys()
or self.data.triggers.keys() - new_data.triggers.keys()
):
self.cleanup_removed_devices(new_data)

View File

@@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pyfritzhome"],
"requirements": ["pyfritzhome==0.6.18"],
"requirements": ["pyfritzhome==0.6.17"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"

View File

@@ -4,17 +4,14 @@ from __future__ import annotations
from typing import Any
from pyfritzhome.devicetypes import FritzhomeTrigger
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import FritzboxConfigEntry
from .entity import FritzBoxDeviceEntity, FritzBoxEntity
from .entity import FritzBoxDeviceEntity
# Coordinator handles data updates, so we can allow unlimited parallel updates
PARALLEL_UPDATES = 0
@@ -29,27 +26,21 @@ async def async_setup_entry(
coordinator = entry.runtime_data
@callback
def _add_entities(
devices: set[str] | None = None, triggers: set[str] | None = None
) -> None:
"""Add devices and triggers."""
def _add_entities(devices: set[str] | None = None) -> None:
"""Add devices."""
if devices is None:
devices = coordinator.new_devices
if triggers is None:
triggers = coordinator.new_triggers
if not devices and not triggers:
if not devices:
return
entities = [
async_add_entities(
FritzboxSwitch(coordinator, ain)
for ain in devices
if coordinator.data.devices[ain].has_switch
] + [FritzboxTrigger(coordinator, ain) for ain in triggers]
async_add_entities(entities)
)
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
_add_entities(set(coordinator.data.devices), set(coordinator.data.triggers))
_add_entities(set(coordinator.data.devices))
class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
@@ -79,42 +70,3 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
translation_domain=DOMAIN,
translation_key="manual_switching_disabled",
)
class FritzboxTrigger(FritzBoxEntity, SwitchEntity):
"""The switch class for FRITZ!SmartHome triggers."""
@property
def data(self) -> FritzhomeTrigger:
"""Return the trigger data entity."""
return self.coordinator.data.triggers[self.ain]
@property
def device_info(self) -> DeviceInfo:
"""Return device specific attributes."""
return DeviceInfo(
name=self.data.name,
identifiers={(DOMAIN, self.ain)},
configuration_url=self.coordinator.configuration_url,
manufacturer="FRITZ!",
model="SmartHome Routine",
)
@property
def is_on(self) -> bool:
"""Return true if the trigger is active."""
return self.data.active # type: ignore [no-any-return]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Activate the trigger."""
await self.hass.async_add_executor_job(
self.coordinator.fritz.set_trigger_active, self.ain
)
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Deactivate the trigger."""
await self.hass.async_add_executor_job(
self.coordinator.fritz.set_trigger_inactive, self.ain
)
await self.coordinator.async_refresh()

View File

@@ -28,9 +28,6 @@
}
},
"number": {
"bbw_dose": {
"default": "mdi:weight-gram"
},
"coffee_temp": {
"default": "mdi:thermometer-water"
},
@@ -54,14 +51,6 @@
}
},
"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": {

View File

@@ -5,14 +5,9 @@ from dataclasses import dataclass
from typing import Any, cast
from pylamarzocco import LaMarzoccoMachine
from pylamarzocco.const import DoseMode, ModelName, PreExtractionMode, WidgetType
from pylamarzocco.const import ModelName, PreExtractionMode, WidgetType
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import (
BrewByWeightDoses,
CoffeeBoiler,
PreBrewing,
SteamBoilerTemperature,
)
from pylamarzocco.models import CoffeeBoiler, PreBrewing, SteamBoilerTemperature
from homeassistant.components.number import (
NumberDeviceClass,
@@ -23,7 +18,6 @@ from homeassistant.const import (
PRECISION_TENTHS,
PRECISION_WHOLE,
EntityCategory,
UnitOfMass,
UnitOfTemperature,
UnitOfTime,
)
@@ -225,72 +219,6 @@ 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)
),
),
)

View File

@@ -5,7 +5,6 @@ from dataclasses import dataclass
from typing import Any, cast
from pylamarzocco.const import (
DoseMode,
ModelName,
PreExtractionMode,
SmartStandByType,
@@ -14,7 +13,7 @@ from pylamarzocco.const import (
)
from pylamarzocco.devices import LaMarzoccoMachine
from pylamarzocco.exceptions import RequestNotSuccessful
from pylamarzocco.models import BrewByWeightDoses, PreBrewing, SteamBoilerLevel
from pylamarzocco.models import PreBrewing, SteamBoilerLevel
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
@@ -51,14 +50,6 @@ 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(
@@ -126,31 +117,6 @@ 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)
),
),
)

View File

@@ -87,9 +87,6 @@
}
},
"number": {
"bbw_dose": {
"name": "Brew by weight {dose}"
},
"coffee_temp": {
"name": "Coffee target temperature"
},
@@ -110,14 +107,6 @@
}
},
"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": {

View File

@@ -1,5 +1,6 @@
"""Support for LCN climate control."""
import asyncio
from collections.abc import Iterable
from datetime import timedelta
from functools import partial
@@ -171,14 +172,14 @@ class LcnClimate(LcnEntity, ClimateEntity):
async def async_update(self) -> None:
"""Update the state of the entity."""
self._attr_available = any(
[
await self.device_connection.request_status_variable(
await asyncio.gather(
self.device_connection.request_status_variable(
self.variable, SCAN_INTERVAL.seconds
),
await self.device_connection.request_status_variable(
self.device_connection.request_status_variable(
self.setpoint, SCAN_INTERVAL.seconds
),
]
)
)
def input_received(self, input_obj: InputType) -> None:

View File

@@ -1,5 +1,6 @@
"""Support for LCN covers."""
import asyncio
from collections.abc import Coroutine, Iterable
from datetime import timedelta
from functools import partial
@@ -133,14 +134,14 @@ class LcnOutputsCover(LcnEntity, CoverEntity):
"""Update the state of the entity."""
if not self.device_connection.is_group:
self._attr_available = any(
[
await self.device_connection.request_status_output(
await asyncio.gather(
self.device_connection.request_status_output(
pypck.lcn_defs.OutputPort["OUTPUTUP"], SCAN_INTERVAL.seconds
),
await self.device_connection.request_status_output(
self.device_connection.request_status_output(
pypck.lcn_defs.OutputPort["OUTPUTDOWN"], SCAN_INTERVAL.seconds
),
]
)
)
def input_received(self, input_obj: InputType) -> None:
@@ -273,7 +274,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
self.motor, self.positioning_mode, SCAN_INTERVAL.seconds
)
)
self._attr_available = any([await coro for coro in coros])
self._attr_available = any(await asyncio.gather(*coros))
def input_received(self, input_obj: InputType) -> None:
"""Set cover states when LCN input object (command) is received."""

View File

@@ -15,13 +15,6 @@ 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,
@@ -33,7 +26,7 @@ async def async_setup_entry(
async_add_entities(
MealieMealplanCalendarEntity(coordinator, entry_type)
for entry_type in SUPPORTED_MEALPLAN_ENTRY_TYPES
for entry_type in MealplanEntryType
)

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["aiomealie==1.1.1"]
"requirements": ["aiomealie==1.1.0"]
}

View File

@@ -9,5 +9,5 @@
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["python-overseerr==0.8.0"]
"requirements": ["python-overseerr==0.7.1"]
}

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from plugwise import GwEntityData
from plugwise.constants import GwEntityData
from homeassistant.const import ATTR_NAME, ATTR_VIA_DEVICE, CONF_HOST
from homeassistant.helpers.device_registry import (
@@ -30,43 +30,37 @@ class PlugwiseEntity(CoordinatorEntity[PlugwiseDataUpdateCoordinator]):
super().__init__(coordinator)
self._dev_id = device_id
api = coordinator.api
gateway_id = api.gateway_id
entry = coordinator.config_entry
configuration_url: str | None = None
if entry := self.coordinator.config_entry:
configuration_url = f"http://{entry.data[CONF_HOST]}"
# 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
data = coordinator.data[device_id]
connections = set()
if mac := self.device.get("mac_address"):
if mac := data.get("mac_address"):
connections.add((CONNECTION_NETWORK_MAC, mac))
if zigbee_mac := self.device.get("zigbee_mac_address"):
connections.add((CONNECTION_ZIGBEE, zigbee_mac))
if mac := data.get("zigbee_mac_address"):
connections.add((CONNECTION_ZIGBEE, mac))
# Set base device info
self._attr_device_info = DeviceInfo(
configuration_url=configuration_url,
identifiers={(DOMAIN, device_id)},
connections=connections,
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"),
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"),
)
# Add extra info if not the gateway device
if device_id != gateway_id:
if device_id != coordinator.api.gateway_id:
self._attr_device_info.update(
{
ATTR_NAME: self.device.get(ATTR_NAME),
ATTR_VIA_DEVICE: (DOMAIN, gateway_id),
ATTR_NAME: data.get(ATTR_NAME),
ATTR_VIA_DEVICE: (
DOMAIN,
str(self.coordinator.api.gateway_id),
),
}
)

View File

@@ -37,6 +37,7 @@ SELECT_TYPES = (
PlugwiseSelectEntityDescription(
key=SELECT_SCHEDULE,
translation_key=SELECT_SCHEDULE,
entity_category=EntityCategory.CONFIG,
options_key="available_schedules",
),
PlugwiseSelectEntityDescription(

View File

@@ -43,12 +43,6 @@ 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__)
@@ -108,9 +102,6 @@ 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:
@@ -178,7 +169,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
self.last_home_update = dt_util.utcnow()
async def _verify_api(self) -> None:
"""Verify that the api is reachable."""
"""Verify that the api is reachable. If it is not, switch clients."""
if self._device.is_connected:
if self._device.is_local_connected:
async_delete_issue(
@@ -226,27 +217,26 @@ 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)
# 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 self.properties_api.status.in_cleaning:
if self._device.is_local_connected:
@@ -258,8 +248,6 @@ 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,
@@ -267,23 +255,6 @@ 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:

View File

@@ -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 (previous state: %s) with"
" last_updated set to %s. Please %s"
" strictly increasing. Triggered by state %s (%s) with last_updated set"
" to %s. Please %s"
),
entity_id,
f"from integration {domain} " if domain else "",

View File

@@ -14,5 +14,13 @@
"turn_on": {
"service": "mdi:bullhorn"
}
},
"triggers": {
"turned_off": {
"trigger": "mdi:bullhorn-outline"
},
"turned_on": {
"trigger": "mdi:bullhorn"
}
}
}

View File

@@ -1,4 +1,8 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted sirens to trigger on.",
"trigger_behavior_name": "Behavior"
},
"entity_component": {
"_": {
"name": "[%key:component::siren::title%]",
@@ -13,6 +17,15 @@
}
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"toggle": {
"description": "Toggles the siren on/off.",
@@ -41,5 +54,27 @@
"name": "[%key:common::action::turn_on%]"
}
},
"title": "Siren"
"title": "Siren",
"triggers": {
"turned_off": {
"description": "Triggers after one or more sirens turn off.",
"fields": {
"behavior": {
"description": "[%key:component::siren::common::trigger_behavior_description%]",
"name": "[%key:component::siren::common::trigger_behavior_name%]"
}
},
"name": "Siren turned off"
},
"turned_on": {
"description": "Triggers after one or more sirens turn on.",
"fields": {
"behavior": {
"description": "[%key:component::siren::common::trigger_behavior_description%]",
"name": "[%key:component::siren::common::trigger_behavior_name%]"
}
},
"name": "Siren turned on"
}
}
}

View File

@@ -0,0 +1,17 @@
"""Provides triggers for sirens."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
from . import DOMAIN
TRIGGERS: dict[str, type[Trigger]] = {
"turned_on": make_entity_state_trigger(DOMAIN, STATE_ON),
"turned_off": make_entity_state_trigger(DOMAIN, STATE_OFF),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for sirens."""
return TRIGGERS

View File

@@ -0,0 +1,18 @@
.trigger_common: &trigger_common
target:
entity:
domain: siren
fields:
behavior:
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
turned_off: *trigger_common
turned_on: *trigger_common

View File

@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.2.7"]
"requirements": ["tesla-fleet-api==1.2.5"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"requirements": ["tesla-fleet-api==1.2.7", "teslemetry-stream==0.7.10"]
"requirements": ["tesla-fleet-api==1.2.5", "teslemetry-stream==0.7.10"]
}

View File

@@ -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.7"]
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.5"]
}

View File

@@ -84,6 +84,12 @@ 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
@@ -155,6 +161,18 @@ 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
@@ -262,8 +280,6 @@ def async_get_hass_or_none() -> HomeAssistant | None:
class ReleaseChannel(enum.StrEnum):
"""Release channel."""
BETA = "beta"
DEV = "dev"
NIGHTLY = "nightly"
@@ -2867,3 +2883,11 @@ 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())

View File

@@ -79,7 +79,7 @@
},
"aemet": {
"name": "AEMET OpenData",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -95,7 +95,7 @@
},
"aftership": {
"name": "AfterShip",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -119,7 +119,7 @@
},
"airnow": {
"name": "AirNow",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -151,7 +151,7 @@
"name": "Airthings"
},
"airthings_ble": {
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling",
"name": "Airthings BLE"
@@ -160,7 +160,7 @@
},
"airtouch4": {
"name": "AirTouch 4",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -276,7 +276,7 @@
},
"amberelectric": {
"name": "Amber Electric",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -323,7 +323,7 @@
},
"android_ip_webcam": {
"name": "Android IP Webcam",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -359,7 +359,7 @@
},
"anthemav": {
"name": "Anthem A/V Receivers",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -505,7 +505,7 @@
},
"arcam_fmj": {
"name": "Arcam FMJ Receivers",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -569,7 +569,7 @@
},
"atag": {
"name": "Atag",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -608,7 +608,7 @@
}
},
"aurora": {
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -620,7 +620,7 @@
},
"aussie_broadband": {
"name": "Aussie Broadband",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -663,7 +663,7 @@
},
"baf": {
"name": "Big Ass Fans",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -680,7 +680,7 @@
},
"balboa": {
"name": "Balboa Spa Client",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -738,7 +738,7 @@
},
"blebox": {
"name": "BleBox devices",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -778,13 +778,13 @@
},
"bluemaestro": {
"name": "BlueMaestro",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
"bluesound": {
"name": "Bluesound",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -865,7 +865,7 @@
},
"brottsplatskartan": {
"name": "Brottsplatskartan",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -928,7 +928,7 @@
},
"buienradar": {
"name": "Buienradar",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -939,7 +939,7 @@
},
"caldav": {
"name": "CalDAV",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -963,7 +963,7 @@
"iot_class": "local_polling"
},
"cert_expiry": {
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -1039,7 +1039,7 @@
},
"cloudflare": {
"name": "Cloudflare",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push",
"single_config_entry": true
@@ -1063,7 +1063,7 @@
},
"coinbase": {
"name": "Coinbase",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -1194,7 +1194,7 @@
},
"daikin": {
"name": "Daikin AC",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -1206,7 +1206,7 @@
},
"datadog": {
"name": "Datadog",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -1326,7 +1326,7 @@
},
"dexcom": {
"name": "Dexcom",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -1395,7 +1395,7 @@
},
"dnsip": {
"name": "DNS IP",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -1407,7 +1407,7 @@
},
"doorbird": {
"name": "DoorBird",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -1448,7 +1448,7 @@
},
"droplet": {
"name": "Droplet",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
},
@@ -1484,7 +1484,7 @@
},
"dunehd": {
"name": "Dune HD",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -1508,7 +1508,7 @@
},
"eafm": {
"name": "Environment Agency Flood Gauges",
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
@@ -1551,7 +1551,7 @@
},
"ecoforest": {
"name": "Ecoforest",
"integration_type": "device",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
@@ -3982,19 +3982,19 @@
"name": "Microsoft",
"integrations": {
"azure_data_explorer": {
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push",
"name": "Azure Data Explorer"
},
"azure_devops": {
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling",
"name": "Azure DevOps"
},
"azure_event_hub": {
"integration_type": "service",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push",
"name": "Azure Event Hub"

10
requirements_all.txt generated
View File

@@ -206,7 +206,7 @@ aioaquacell==0.2.0
aioaseko==1.0.0
# homeassistant.components.asuswrt
aioasuswrt==1.5.3
aioasuswrt==1.5.2
# 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.1
aiomealie==1.1.0
# homeassistant.components.modern_forms
aiomodernforms==0.1.8
@@ -2061,7 +2061,7 @@ pyforked-daapd==0.1.14
pyfreedompro==1.1.0
# homeassistant.components.fritzbox
pyfritzhome==0.6.18
pyfritzhome==0.6.17
# homeassistant.components.ifttt
pyfttt==0.3
@@ -2560,7 +2560,7 @@ python-opensky==1.0.1
python-otbr-api==2.7.0
# homeassistant.components.overseerr
python-overseerr==0.8.0
python-overseerr==0.7.1
# 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.7
tesla-fleet-api==1.2.5
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View File

@@ -197,7 +197,7 @@ aioaquacell==0.2.0
aioaseko==1.0.0
# homeassistant.components.asuswrt
aioasuswrt==1.5.3
aioasuswrt==1.5.2
# 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.1
aiomealie==1.1.0
# homeassistant.components.modern_forms
aiomodernforms==0.1.8
@@ -1741,7 +1741,7 @@ pyforked-daapd==0.1.14
pyfreedompro==1.1.0
# homeassistant.components.fritzbox
pyfritzhome==0.6.18
pyfritzhome==0.6.17
# homeassistant.components.ifttt
pyfttt==0.3
@@ -2144,7 +2144,7 @@ python-opensky==1.0.1
python-otbr-api==2.7.0
# homeassistant.components.overseerr
python-overseerr==0.8.0
python-overseerr==0.7.1
# 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.7
tesla-fleet-api==1.2.5
# homeassistant.components.powerwall
tesla-powerwall==0.5.2

View File

@@ -55,55 +55,6 @@
'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({

View File

@@ -16,7 +16,6 @@ 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,

View File

@@ -25,7 +25,6 @@ async def setup_config_entry(
device: Mock | None = None,
fritz: Mock | None = None,
template: Mock | None = None,
trigger: Mock | None = None,
) -> MockConfigEntry:
"""Do setup of a MockConfigEntry."""
entry = MockConfigEntry(
@@ -40,9 +39,6 @@ async def setup_config_entry(
if template is not None and fritz is not None:
fritz().get_templates.return_value = [template]
if trigger is not None and fritz is not None:
fritz().get_triggers.return_value = [trigger]
await hass.config_entries.async_setup(entry.entry_id)
if device is not None:
await hass.async_block_till_done()
@@ -50,10 +46,7 @@ async def setup_config_entry(
def set_devices(
fritz: Mock,
devices: list[Mock] | None = None,
templates: list[Mock] | None = None,
triggers: list[Mock] | None = None,
fritz: Mock, devices: list[Mock] | None = None, templates: list[Mock] | None = None
) -> None:
"""Set list of devices or templates."""
if devices is not None:
@@ -62,9 +55,6 @@ def set_devices(
if templates is not None:
fritz().get_templates.return_value = templates
if triggers is not None:
fritz().get_triggers.return_value = triggers
class FritzEntityBaseMock(Mock):
"""base mock of a AVM Fritz!Box binary sensor device."""
@@ -209,11 +199,3 @@ class FritzDeviceCoverUnknownPositionMock(FritzDeviceCoverMock):
"""Mock of a AVM Fritz!Box cover device with unknown position."""
levelpercentage = None
class FritzTriggerMock(FritzEntityBaseMock):
"""Mock of a AVM Fritz!Box smarthome trigger."""
active = True
ain = "trg1234 56789"
name = "fake_trigger"

View File

@@ -47,51 +47,3 @@
'state': 'on',
})
# ---
# name: test_setup[switch.fake_trigger-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': 'switch',
'entity_category': None,
'entity_id': 'switch.fake_trigger',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'fake_trigger',
'platform': 'fritzbox',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'trg1234 56789',
'unit_of_measurement': None,
})
# ---
# name: test_setup[switch.fake_trigger-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'fake_trigger',
}),
'context': <ANY>,
'entity_id': 'switch.fake_trigger',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@@ -23,13 +23,12 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.util import dt as dt_util
from . import FritzDeviceSwitchMock, FritzTriggerMock, set_devices, setup_config_entry
from . import FritzDeviceSwitchMock, set_devices, setup_config_entry
from .const import CONF_FAKE_NAME, MOCK_CONFIG
from tests.common import async_fire_time_changed, snapshot_platform
SWITCH_ENTITY_ID = f"{SWITCH_DOMAIN}.{CONF_FAKE_NAME}"
TRIGGER_ENTITY_ID = f"{SWITCH_DOMAIN}.fake_trigger"
ENTITY_ID = f"{SWITCH_DOMAIN}.{CONF_FAKE_NAME}"
async def test_setup(
@@ -40,56 +39,50 @@ async def test_setup(
) -> None:
"""Test setup of platform."""
device = FritzDeviceSwitchMock()
trigger = FritzTriggerMock()
with patch("homeassistant.components.fritzbox.PLATFORMS", [Platform.SWITCH]):
entry = await setup_config_entry(
hass,
MOCK_CONFIG[DOMAIN][CONF_DEVICES][0],
device=device,
fritz=fritz,
trigger=trigger,
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
assert entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)
async def test_switch_turn_on(hass: HomeAssistant, fritz: Mock) -> None:
"""Test turn switch device on."""
async def test_turn_on(hass: HomeAssistant, fritz: Mock) -> None:
"""Test turn device on."""
device = FritzDeviceSwitchMock()
await setup_config_entry(
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, True
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
assert device.set_switch_state_on.call_count == 1
async def test_switch_turn_off(hass: HomeAssistant, fritz: Mock) -> None:
"""Test turn switch device off."""
async def test_turn_off(hass: HomeAssistant, fritz: Mock) -> None:
"""Test turn device off."""
device = FritzDeviceSwitchMock()
await setup_config_entry(
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, True
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
assert device.set_switch_state_off.call_count == 1
async def test_switch_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> None:
"""Test toggling while switch device is locked."""
async def test_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> None:
"""Test toggling while device is locked."""
device = FritzDeviceSwitchMock()
device.lock = True
await setup_config_entry(
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
with pytest.raises(
@@ -97,7 +90,7 @@ async def test_switch_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> N
match="Can't toggle switch while manual switching is disabled for the device",
):
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, True
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
with pytest.raises(
@@ -105,23 +98,17 @@ async def test_switch_toggle_while_locked(hass: HomeAssistant, fritz: Mock) -> N
match="Can't toggle switch while manual switching is disabled for the device",
):
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: SWITCH_ENTITY_ID}, True
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
"""Test update without error."""
device = FritzDeviceSwitchMock()
trigger = FritzTriggerMock()
await setup_config_entry(
hass,
MOCK_CONFIG[DOMAIN][CONF_DEVICES][0],
device=device,
fritz=fritz,
trigger=trigger,
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
assert fritz().update_devices.call_count == 1
assert fritz().update_triggers.call_count == 1
assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200)
@@ -129,7 +116,6 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None:
await hass.async_block_till_done(wait_background_tasks=True)
assert fritz().update_devices.call_count == 2
assert fritz().update_triggers.call_count == 2
assert fritz().login.call_count == 1
@@ -138,7 +124,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None:
device = FritzDeviceSwitchMock()
fritz().update_devices.side_effect = HTTPError("Boom")
entry = await setup_config_entry(
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
assert entry.state is ConfigEntryState.SETUP_RETRY
assert fritz().update_devices.call_count == 2
@@ -159,10 +145,10 @@ async def test_assume_device_unavailable(hass: HomeAssistant, fritz: Mock) -> No
device.energy = 0
device.power = 0
await setup_config_entry(
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], device=device, fritz=fritz
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
state = hass.states.get(SWITCH_ENTITY_ID)
state = hass.states.get(ENTITY_ID)
assert state
assert state.state == STATE_UNAVAILABLE
@@ -170,19 +156,13 @@ async def test_assume_device_unavailable(hass: HomeAssistant, fritz: Mock) -> No
async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
"""Test adding new discovered devices during runtime."""
device = FritzDeviceSwitchMock()
trigger = FritzTriggerMock()
await setup_config_entry(
hass,
MOCK_CONFIG[DOMAIN][CONF_DEVICES][0],
device=device,
fritz=fritz,
trigger=trigger,
hass, MOCK_CONFIG[DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
)
assert hass.states.get(SWITCH_ENTITY_ID)
assert hass.states.get(TRIGGER_ENTITY_ID)
state = hass.states.get(ENTITY_ID)
assert state
# add new switch device
new_device = FritzDeviceSwitchMock()
new_device.ain = "7890 1234"
new_device.name = "new_switch"
@@ -192,48 +172,5 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done(wait_background_tasks=True)
assert hass.states.get(f"{SWITCH_DOMAIN}.new_switch")
# add new trigger
new_trigger = FritzTriggerMock()
new_trigger.ain = "trg7890 1234"
new_trigger.name = "new_trigger"
set_devices(fritz, triggers=[trigger, new_trigger])
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done(wait_background_tasks=True)
assert hass.states.get(f"{SWITCH_DOMAIN}.new_trigger")
async def test_activate_trigger(hass: HomeAssistant, fritz: Mock) -> None:
"""Test activating a FRITZ! trigger."""
trigger = FritzTriggerMock()
await setup_config_entry(
hass,
MOCK_CONFIG[DOMAIN][CONF_DEVICES][0],
fritz=fritz,
trigger=trigger,
)
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TRIGGER_ENTITY_ID}, True
)
assert fritz().set_trigger_active.call_count == 1
async def test_deactivate_trigger(hass: HomeAssistant, fritz: Mock) -> None:
"""Test deactivating a FRITZ! trigger."""
trigger = FritzTriggerMock()
await setup_config_entry(
hass,
MOCK_CONFIG[DOMAIN][CONF_DEVICES][0],
fritz=fritz,
trigger=trigger,
)
await hass.services.async_call(
SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: TRIGGER_ENTITY_ID}, True
)
assert fritz().set_trigger_inactive.call_count == 1
state = hass.states.get(f"{SWITCH_DOMAIN}.new_switch")
assert state

View File

@@ -163,7 +163,7 @@
"code": "CMBrewByWeightDoses",
"index": 1,
"output": {
"scaleConnected": true,
"scaleConnected": false,
"availableModes": ["Continuous"],
"mode": "Continuous",
"doses": {

View File

@@ -1,122 +1,4 @@
# 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({

Some files were not shown because too many files have changed in this diff Show More