Compare commits

...

40 Commits

Author SHA1 Message Date
Daniel Hjelseth Høyer
205bc0456f Merge branch 'dev' into homevolt 2026-01-20 16:24:32 +01:00
Erik Montnemery
1730479c8d Remove reference of removed stub_blueprint_populate fixture from siren tests (#161294) 2026-01-20 16:16:22 +01:00
Thomas55555
bc28c8fd3c Add ppb as a valid uom for sensor/number CO device class (#159554) 2026-01-20 16:07:24 +01:00
Erik Montnemery
c3616fd5df Add siren conditions (#161021) 2026-01-20 16:05:21 +01:00
epenet
6b97f2ac06 Use shorthand attributes in wyoming TTS (#161286) 2026-01-20 15:59:49 +01:00
Erik Montnemery
deefcbcbe4 Remove stub_blueprint_populate test fixture (#161288) 2026-01-20 15:46:06 +01:00
Samuel Xiao
e84aeb9f99 Switchbot Cloud: Add new supported Lock (#161276) 2026-01-20 15:27:27 +01:00
epenet
ade3d8a657 Pass timestamps to Tuya wrapper skip_update (#161271) 2026-01-20 15:24:49 +01:00
Krisjanis Lejejs
a65d9032ff Bump hass-nabucasa from 1.10.0 to 1.11.0 (#161283) 2026-01-20 14:50:00 +01:00
Martin Hjelmare
b950a4eaf4 Fix nobo_hub options flow unload mocking (#161287) 2026-01-20 14:49:46 +01:00
Joost Lekkerkerker
3fe91751f5 Make add entry translatable (#159901) 2026-01-20 14:25:33 +01:00
Erwin Douna
6ee58b96ca Bump pyfirefly to 0.1.12 (#161278) 2026-01-20 13:51:09 +01:00
Erik Montnemery
d1404e7905 Simplify logic in condition tests (#161239) 2026-01-20 10:39:47 +01:00
Paulus Schoutsen
7c34191813 Use new app panel instead of ingress page (#161264)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-01-20 10:54:56 +02:00
PolarBearEs
7540d04779 Remove duplicated MQTT_ORIGIN_INFO_SCHEMA in schemas.py (#161263) 2026-01-20 08:41:40 +01:00
mettolen
d828130670 Bump pysaunum to 0.3.0 (#161255) 2026-01-20 08:31:48 +01:00
Adalberto Garcia Garces
2ec6c08bd7 Add 18 new Tuya device fixtures (#161225) 2026-01-20 07:33:59 +01:00
Thomas55555
48852bab7a Add ppb as a valid UOM for sensor/number SO2 device class (#159431) 2026-01-19 23:32:45 +00:00
Andres Ruiz
7d370f4513 Bump waterfurnace to 1.4.0 (#161244) 2026-01-19 20:33:29 +01:00
Aleksandr Oleinikov
9d97791faf Change default model for Ollama to qwen3:4b-instruct (#161202)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-01-19 12:56:18 -05:00
Norbert Rittel
4fe8982b68 Clarify description of lawn_mower.docked trigger (#161238) 2026-01-19 17:48:40 +00:00
Daniel Hjelseth Høyer
5aa32491c8 Merge branch 'dev' into homevolt 2026-01-15 16:25:46 +01:00
Daniel Hjelseth Høyer
dc2cd2246b Merge branch 'dev' into homevolt 2026-01-15 07:06:51 +01:00
Daniel Hjelseth Høyer
181037820b Merge branch 'dev' into homevolt 2026-01-14 21:05:45 +01:00
Daniel Hjelseth Høyer
6cf15bf70c homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 19:09:37 +01:00
Daniel Hjelseth Høyer
5a34c31e42 Merge branch 'dev' into homevolt 2026-01-14 18:30:20 +01:00
Daniel Hjelseth Høyer
9dcc86f12e homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 18:03:21 +01:00
Daniel Hjelseth Høyer
04429a6eef homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 17:40:51 +01:00
Daniel Hjelseth Høyer
51e2506afb homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 16:41:08 +01:00
Daniel Hjelseth Høyer
e49e5c7c40 Merge branch 'dev' into homevolt 2026-01-14 14:41:26 +01:00
Daniel Hjelseth Høyer
b8dfc523da homevolt
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-14 14:36:43 +01:00
Daniel Hjelseth Høyer
a25fbf57ef Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 17:20:27 +01:00
Daniel Hjelseth Høyer
dac22002b0 Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 14:53:07 +01:00
Daniel Hjelseth Høyer
e61f00a3ae Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 14:15:56 +01:00
Daniel Hjelseth Høyer
14a67c6b5d Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:46:49 +01:00
Daniel Hjelseth Høyer
90ae81f02b Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:39:46 +01:00
Daniel Hjelseth Høyer
a741f214da Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:35:53 +01:00
Daniel Hjelseth Høyer
21d0bd3ce2 Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:22:32 +01:00
Daniel Hjelseth Høyer
d9c1f4850a Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 10:09:50 +01:00
Daniel Hjelseth Høyer
335994af7e Add Homevolt integration
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-07 09:44:06 +01:00
196 changed files with 9324 additions and 763 deletions

2
CODEOWNERS generated
View File

@@ -711,6 +711,8 @@ build.json @home-assistant/supervisor
/tests/components/homematic/ @pvizeli
/homeassistant/components/homematicip_cloud/ @hahn-th
/tests/components/homematicip_cloud/ @hahn-th
/homeassistant/components/homevolt/ @danielhiversen
/tests/components/homevolt/ @danielhiversen
/homeassistant/components/homewizard/ @DCSBL
/tests/components/homewizard/ @DCSBL
/homeassistant/components/honeywell/ @rdfurman @mkmer

View File

@@ -52,7 +52,7 @@ class AdGuardHomeEntity(Entity):
def device_info(self) -> DeviceInfo:
"""Return device information about this AdGuard Home instance."""
if self._entry.source == SOURCE_HASSIO:
config_url = "homeassistant://hassio/ingress/a0d7b954_adguard"
config_url = "homeassistant://app/a0d7b954_adguard"
elif self.adguard.tls:
config_url = f"https://{self.adguard.host}:{self.adguard.port}"
else:

View File

@@ -127,6 +127,7 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
"assist_satellite",
"fan",
"light",
"siren",
}
_EXPERIMENTAL_TRIGGER_PLATFORMS = {

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.10.0"],
"requirements": ["hass-nabucasa==1.11.0"],
"single_config_entry": true
}

View File

@@ -10,7 +10,7 @@ LOGGER = logging.getLogger(__package__)
DOMAIN = "deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://hassio/ingress/core_deconz"
HASSIO_CONFIGURATION_URL = "homeassistant://app/core_deconz"
CONF_BRIDGE_ID = "bridgeid"
CONF_GROUP_ID_BASE = "group_id_base"

View File

@@ -1034,7 +1034,7 @@ def _async_setup_device_registry(
and dashboard.data
and dashboard.data.get(device_info.name)
):
configuration_url = f"homeassistant://hassio/ingress/{dashboard.addon_slug}"
configuration_url = f"homeassistant://app/{dashboard.addon_slug}"
manufacturer = "espressif"
if device_info.manufacturer:

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyfirefly==0.1.11"]
"requirements": ["pyfirefly==0.1.12"]
}

View File

@@ -0,0 +1,36 @@
"""The Homevolt integration."""
from __future__ import annotations
from homevolt import Homevolt
from homeassistant.const import CONF_HOST, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool:
"""Set up Homevolt from a config entry."""
host: str = entry.data[CONF_HOST]
password: str | None = entry.data.get(CONF_PASSWORD)
websession = async_get_clientsession(hass)
client = Homevolt(host, password, websession=websession)
coordinator = HomevoltDataUpdateCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HomevoltConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -0,0 +1,70 @@
"""Config flow for the Homevolt integration."""
from __future__ import annotations
import logging
from typing import Any
from homevolt import Homevolt, HomevoltAuthenticationError, HomevoltConnectionError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Optional(CONF_PASSWORD): str,
}
)
class HomevoltConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Homevolt."""
VERSION = 1
MINOR_VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
host = user_input[CONF_HOST]
password = user_input.get(CONF_PASSWORD)
websession = async_get_clientsession(self.hass)
client = Homevolt(host, password, websession=websession)
try:
await client.update_info()
device = client.get_device()
device_id = device.device_id
except HomevoltAuthenticationError:
errors["base"] = "invalid_auth"
except HomevoltConnectionError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception(
"Error occurred while connecting to the Homevolt battery"
)
errors["base"] = "unknown"
else:
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="Homevolt Local",
data={
CONF_HOST: host,
CONF_PASSWORD: password,
},
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -0,0 +1,9 @@
"""Constants for the Homevolt integration."""
from __future__ import annotations
from datetime import timedelta
DOMAIN = "homevolt"
MANUFACTURER = "Homevolt"
SCAN_INTERVAL = timedelta(seconds=15)

View File

@@ -0,0 +1,56 @@
"""Data update coordinator for Homevolt integration."""
from __future__ import annotations
import logging
from homevolt import (
Device,
Homevolt,
HomevoltAuthenticationError,
HomevoltConnectionError,
HomevoltError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, SCAN_INTERVAL
type HomevoltConfigEntry = ConfigEntry[HomevoltDataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
class HomevoltDataUpdateCoordinator(DataUpdateCoordinator[Device]):
"""Class to manage fetching Homevolt data."""
config_entry: HomevoltConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: HomevoltConfigEntry,
client: Homevolt,
) -> None:
"""Initialize the Homevolt coordinator."""
self.client = client
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
config_entry=entry,
)
async def _async_update_data(self) -> Device:
"""Fetch data from the Homevolt API."""
try:
await self.client.update_info()
return self.client.get_device()
except HomevoltAuthenticationError as err:
raise ConfigEntryAuthFailed from err
except (HomevoltConnectionError, HomevoltError) as err:
raise UpdateFailed(f"Error communicating with device: {err}") from err

View File

@@ -0,0 +1,12 @@
{
"domain": "homevolt",
"name": "Homevolt",
"codeowners": ["@danielhiversen"],
"config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/homevolt",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["homevolt==0.2.4"]
}

View File

@@ -0,0 +1,70 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Local_polling without events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: Integration does not register custom actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: Integration does not have an options flow.
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -0,0 +1,162 @@
"""Support for Homevolt sensors."""
from __future__ import annotations
from homevolt.models import SensorType
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .coordinator import HomevoltConfigEntry, HomevoltDataUpdateCoordinator
PARALLEL_UPDATES = 0 # Coordinator-based updates
SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key=SensorType.COUNT,
state_class=SensorStateClass.TOTAL_INCREASING,
),
SensorEntityDescription(
key=SensorType.ENERGY_TOTAL,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
),
SensorEntityDescription(
key=SensorType.ENERGY_INCREASING,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
),
SensorEntityDescription(
key=SensorType.FREQUENCY,
device_class=SensorDeviceClass.FREQUENCY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfFrequency.HERTZ,
),
SensorEntityDescription(
key=SensorType.PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
SensorEntityDescription(
key=SensorType.POWER,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
),
SensorEntityDescription(
key=SensorType.SCHEDULE_TYPE,
),
SensorEntityDescription(
key=SensorType.SIGNAL_STRENGTH,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
),
SensorEntityDescription(
key=SensorType.TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
SensorEntityDescription(
key=SensorType.TEXT,
),
SensorEntityDescription(
key=SensorType.VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
),
SensorEntityDescription(
key=SensorType.CURRENT,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: HomevoltConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Homevolt sensor."""
coordinator = entry.runtime_data
entities = []
sensors_by_key = {sensor.key: sensor for sensor in SENSORS}
for sensor_key, sensor in coordinator.data.sensors.items():
if (description := sensors_by_key.get(sensor.type)) is None:
continue
entities.append(
HomevoltSensor(
description,
coordinator,
sensor_key,
)
)
async_add_entities(entities)
class HomevoltSensor(CoordinatorEntity[HomevoltDataUpdateCoordinator], SensorEntity):
"""Representation of a Homevolt sensor."""
_attr_has_entity_name = True
def __init__(
self,
description: SensorEntityDescription,
coordinator: HomevoltDataUpdateCoordinator,
sensor_key: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
device_id = coordinator.data.device_id
self._attr_unique_id = f"{device_id}_{sensor_key}"
sensor_data = coordinator.data.sensors[sensor_key]
self._attr_translation_key = sensor_data.slug
self._sensor_key = sensor_key
device_metadata = coordinator.data.device_metadata.get(
sensor_data.device_identifier
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{device_id}_{sensor_data.device_identifier}")},
configuration_url=coordinator.client.base_url,
manufacturer=MANUFACTURER,
model=device_metadata.model if device_metadata else None,
name=device_metadata.name if device_metadata else None,
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._sensor_key in self.coordinator.data.sensors
@property
def native_value(self) -> StateType:
"""Return the native value of the sensor."""
return self.coordinator.data.sensors[self._sensor_key].value

View File

@@ -0,0 +1,198 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "The IP address or hostname of your Homevolt battery on your local network.",
"password": "The local password configured for your Homevolt battery, if required."
},
"description": "Connect Home Assistant to your Homevolt battery over the local network.",
"title": "Homevolt Local"
}
}
},
"entity": {
"sensor": {
"available_charging_energy": {
"name": "Available charging energy"
},
"available_charging_power": {
"name": "Available charging power"
},
"available_discharge_energy": {
"name": "Available discharge energy"
},
"available_discharge_power": {
"name": "Available discharge power"
},
"average_rssi_grid": {
"name": "Grid average RSSI"
},
"average_rssi_load": {
"name": "Load average RSSI"
},
"battery_state_of_charge": {
"name": "Battery state of charge"
},
"charge_cycles": {
"name": "Charge cycles"
},
"energy_exported_grid": {
"name": "Grid exported energy"
},
"energy_exported_load": {
"name": "Load exported energy"
},
"energy_imported_grid": {
"name": "Grid imported energy"
},
"energy_imported_load": {
"name": "Load imported energy"
},
"exported_energy": {
"name": "Exported energy"
},
"frequency": {
"name": "Frequency"
},
"imported_energy": {
"name": "Imported energy"
},
"l1_current": {
"name": "L1 current"
},
"l1_current_grid": {
"name": "Grid L1 current"
},
"l1_current_load": {
"name": "Load L1 current"
},
"l1_l2_voltage": {
"name": "L1-L2 voltage"
},
"l1_power_grid": {
"name": "Grid L1 power"
},
"l1_power_load": {
"name": "Load L1 power"
},
"l1_voltage": {
"name": "L1 voltage"
},
"l1_voltage_grid": {
"name": "Grid L1 voltage"
},
"l1_voltage_load": {
"name": "Load L1 voltage"
},
"l2_current": {
"name": "L2 current"
},
"l2_current_grid": {
"name": "Grid L2 current"
},
"l2_current_load": {
"name": "Load L2 current"
},
"l2_l3_voltage": {
"name": "L2-L3 voltage"
},
"l2_power_grid": {
"name": "Grid L2 power"
},
"l2_power_load": {
"name": "Load L2 power"
},
"l2_voltage": {
"name": "L2 voltage"
},
"l2_voltage_grid": {
"name": "Grid L2 voltage"
},
"l2_voltage_load": {
"name": "Load L2 voltage"
},
"l3_current": {
"name": "L3 current"
},
"l3_current_grid": {
"name": "Grid L3 current"
},
"l3_current_load": {
"name": "Load L3 current"
},
"l3_l1_voltage": {
"name": "L3-L1 voltage"
},
"l3_power_grid": {
"name": "Grid L3 power"
},
"l3_power_load": {
"name": "Load L3 power"
},
"l3_voltage": {
"name": "L3 voltage"
},
"l3_voltage_grid": {
"name": "Grid L3 voltage"
},
"l3_voltage_load": {
"name": "Load L3 voltage"
},
"power": {
"name": "Power"
},
"power_grid": {
"name": "Grid power"
},
"power_load": {
"name": "Load power"
},
"rssi_grid": {
"name": "Grid RSSI"
},
"rssi_load": {
"name": "Load RSSI"
},
"schedule_id": {
"name": "Schedule ID"
},
"schedule_max_discharge": {
"name": "Schedule max discharge"
},
"schedule_max_power": {
"name": "Schedule max power"
},
"schedule_power_setpoint": {
"name": "Schedule power setpoint"
},
"schedule_type": {
"name": "Schedule type"
},
"state_of_charge": {
"name": "State of charge"
},
"system_temperature": {
"name": "System temperature"
},
"tmax": {
"name": "Maximum temperature"
},
"tmin": {
"name": "Minimum temperature"
}
}
}
}

View File

@@ -41,7 +41,7 @@
"title": "Lawn mower",
"triggers": {
"docked": {
"description": "Triggers after one or more lawn mowers return to dock.",
"description": "Triggers after one or more lawn mowers have returned to dock.",
"fields": {
"behavior": {
"description": "[%key:component::lawn_mower::common::trigger_behavior_description%]",

View File

@@ -50,7 +50,7 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
"""Check connection to the Mealie API."""
assert self.host is not None
if "/hassio/ingress/" in self.host:
if "/app/" in self.host:
return {"base": "ingress_url"}, None
client = MealieClient(

View File

@@ -73,15 +73,6 @@ SHARED_OPTIONS = [
CONF_STATE_TOPIC,
]
MQTT_ORIGIN_INFO_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_SW_VERSION): cv.string,
vol.Optional(CONF_SUPPORT_URL): cv.configuration_url,
}
),
)
_MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema(
{

View File

@@ -125,7 +125,7 @@ class NumberDeviceClass(StrEnum):
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
Unit of measurement: `ppm` (parts per million), `mg/m³`, `μg/m³`
Unit of measurement: `ppb` (parts per billion), `ppm` (parts per million), `mg/m³`, `μg/m³`
"""
CO2 = "carbon_dioxide"
@@ -373,7 +373,7 @@ class NumberDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `μg/m³`
Unit of measurement: `ppb` (parts per billion), `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -483,6 +483,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.BATTERY: {PERCENTAGE},
NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
NumberDeviceClass.CO: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -545,7 +546,10 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
},
NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
NumberDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
NumberDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature),
NumberDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {

View File

@@ -158,7 +158,7 @@ MODEL_NAMES = [ # https://ollama.com/library
"yi",
"zephyr",
]
DEFAULT_MODEL = "qwen3:4b"
DEFAULT_MODEL = "qwen3:4b-instruct"
DEFAULT_CONVERSATION_NAME = "Ollama Conversation"
DEFAULT_AI_TASK_NAME = "Ollama AI Task"

View File

@@ -64,6 +64,7 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -225,6 +226,7 @@ _PRIMARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
_SECONDARY_UNIT_CONVERTERS: list[type[BaseUnitConverter]] = [
CarbonMonoxideConcentrationConverter,
TemperatureDeltaConverter,
SulphurDioxideConcentrationConverter,
]
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {

View File

@@ -38,6 +38,7 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -94,6 +95,9 @@ UNIT_SCHEMA = vol.Schema(
vol.Optional("reactive_energy"): vol.In(ReactiveEnergyConverter.VALID_UNITS),
vol.Optional("reactive_power"): vol.In(ReactivePowerConverter.VALID_UNITS),
vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS),
vol.Optional("sulphur_dioxide"): vol.In(
SulphurDioxideConcentrationConverter.VALID_UNITS
),
vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS),
vol.Optional("temperature_delta"): vol.In(
TemperatureDeltaConverter.VALID_UNITS

View File

@@ -44,6 +44,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) ->
async def async_unload_entry(hass: HomeAssistant, entry: LeilSaunaConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
entry.runtime_data.client.close()
await entry.runtime_data.client.async_close()
return unload_ok

View File

@@ -51,7 +51,7 @@ async def validate_input(data: dict[str, Any]) -> None:
# Try to read data to verify communication
await client.async_get_data()
finally:
client.close()
await client.async_close()
class LeilSaunaConfigFlow(ConfigFlow, domain=DOMAIN):

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["pysaunum"],
"quality_scale": "platinum",
"requirements": ["pysaunum==0.2.0"]
"requirements": ["pysaunum==0.3.0"]
}

View File

@@ -68,6 +68,7 @@ from homeassistant.util.unit_conversion import (
ReactiveEnergyConverter,
ReactivePowerConverter,
SpeedConverter,
SulphurDioxideConcentrationConverter,
TemperatureConverter,
TemperatureDeltaConverter,
UnitlessRatioConverter,
@@ -158,7 +159,7 @@ class SensorDeviceClass(StrEnum):
CO = "carbon_monoxide"
"""Carbon Monoxide gas concentration.
Unit of measurement: `ppm` (parts per million), `mg/m³`, `μg/m³`
Unit of measurement: `ppb` (parts per billion), `ppm` (parts per million), `mg/m³`, `μg/m³`
"""
CO2 = "carbon_dioxide"
@@ -409,7 +410,7 @@ class SensorDeviceClass(StrEnum):
SULPHUR_DIOXIDE = "sulphur_dioxide"
"""Amount of SO2.
Unit of measurement: `μg/m³`
Unit of measurement: `ppb` (parts per billion), `μg/m³`
"""
TEMPERATURE = "temperature"
@@ -569,6 +570,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
SensorDeviceClass.PRESSURE: PressureConverter,
SensorDeviceClass.REACTIVE_ENERGY: ReactiveEnergyConverter,
SensorDeviceClass.REACTIVE_POWER: ReactivePowerConverter,
SensorDeviceClass.SULPHUR_DIOXIDE: SulphurDioxideConcentrationConverter,
SensorDeviceClass.SPEED: SpeedConverter,
SensorDeviceClass.TEMPERATURE: TemperatureConverter,
SensorDeviceClass.TEMPERATURE_DELTA: TemperatureDeltaConverter,
@@ -595,6 +597,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.BATTERY: {PERCENTAGE},
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
SensorDeviceClass.CO: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@@ -657,7 +660,10 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
},
SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
SensorDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
SensorDeviceClass.SULPHUR_DIOXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature),
SensorDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {

View File

@@ -0,0 +1,17 @@
"""Provides conditions for sirens."""
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from . import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
"""Return the siren conditions."""
return CONDITIONS

View File

@@ -0,0 +1,17 @@
.condition_common: &condition_common
target:
entity:
domain: siren
fields:
behavior:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
is_off: *condition_common
is_on: *condition_common

View File

@@ -1,4 +1,12 @@
{
"conditions": {
"is_off": {
"condition": "mdi:bullhorn-outline"
},
"is_on": {
"condition": "mdi:bullhorn"
}
},
"entity_component": {
"_": {
"default": "mdi:bullhorn"

View File

@@ -1,8 +1,32 @@
{
"common": {
"condition_behavior_description": "How the state should match on the targeted sirens.",
"condition_behavior_name": "Behavior",
"trigger_behavior_description": "The behavior of the targeted sirens to trigger on.",
"trigger_behavior_name": "Behavior"
},
"conditions": {
"is_off": {
"description": "Tests if one or more sirens are off.",
"fields": {
"behavior": {
"description": "[%key:component::siren::common::condition_behavior_description%]",
"name": "[%key:component::siren::common::condition_behavior_name%]"
}
},
"name": "If a siren is off"
},
"is_on": {
"description": "Tests if one or more sirens are on.",
"fields": {
"behavior": {
"description": "[%key:component::siren::common::condition_behavior_description%]",
"name": "[%key:component::siren::common::condition_behavior_name%]"
}
},
"name": "If a siren is on"
}
},
"entity_component": {
"_": {
"name": "[%key:component::siren::title%]",
@@ -18,6 +42,12 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",

View File

@@ -16,6 +16,9 @@
"create_entry": {
"default": "Successfully authenticated with Spotify."
},
"initiate_flow": {
"user": "[%key:common::config_flow::initiate_flow::account%]"
},
"step": {
"pick_implementation": {
"data": {

View File

@@ -185,6 +185,9 @@ async def make_device_data(
"Smart Lock Lite",
"Smart Lock Pro",
"Smart Lock Ultra",
"Smart Lock Vision",
"Smart Lock Vision Pro",
"Smart Lock Pro Wifi",
]:
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id

View File

@@ -92,6 +92,18 @@ BINARY_SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Smart Lock Vision": (
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Smart Lock Vision Pro": (
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Smart Lock Pro Wifi": (
CALIBRATION_DESCRIPTION,
DOOR_OPEN_DESCRIPTION,
),
"Curtain": (CALIBRATION_DESCRIPTION,),
"Curtain3": (CALIBRATION_DESCRIPTION,),
"Roller Shade": (CALIBRATION_DESCRIPTION,),

View File

@@ -46,7 +46,7 @@ class SwitchBotCloudLock(SwitchBotCloudEntity, LockEntity):
"""Set attributes from coordinator data."""
if coord_data := self.coordinator.data:
self._attr_is_locked = coord_data["lockState"] == "locked"
if self.__model in LockV2Commands.get_supported_devices():
if self.__model != "Smart Lock Lite":
self._attr_supported_features = LockEntityFeature.OPEN
async def async_lock(self, **kwargs: Any) -> None:

View File

@@ -225,6 +225,9 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
"Smart Lock Lite": (BATTERY_DESCRIPTION,),
"Smart Lock Pro": (BATTERY_DESCRIPTION,),
"Smart Lock Ultra": (BATTERY_DESCRIPTION,),
"Smart Lock Vision": (BATTERY_DESCRIPTION,),
"Smart Lock Vision Pro": (BATTERY_DESCRIPTION,),
"Smart Lock Pro Wifi": (BATTERY_DESCRIPTION,),
"Relay Switch 2PM": (
RELAY_SWITCH_2PM_POWER_DESCRIPTION,
RELAY_SWITCH_2PM_VOLTAGE_DESCRIPTION,

View File

@@ -165,7 +165,7 @@ class DeviceListener(SharingDeviceListener):
self,
device: CustomerDevice,
updated_status_properties: list[str] | None = None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None = None,
) -> None:
"""Update device status with optional DP timestamps."""
LOGGER.debug(

View File

@@ -471,9 +471,11 @@ class TuyaBinarySensorEntity(TuyaEntity, BinarySensorEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
return
self.async_write_ha_state()

View File

@@ -57,7 +57,7 @@ class TuyaEntity(Entity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
self.async_write_ha_state()

View File

@@ -218,10 +218,10 @@ class TuyaEventEntity(TuyaEntity, EventEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties
self.device, updated_status_properties, dp_timestamps
) or not (event_data := self._dpcode_wrapper.read_device_status(self.device)):
return

View File

@@ -31,7 +31,10 @@ class DeviceWrapper[T]:
options: list[str]
def skip_update(
self, device: CustomerDevice, updated_status_properties: list[str] | None
self,
device: CustomerDevice,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
) -> bool:
"""Determine if the wrapper should skip an update.
@@ -62,7 +65,10 @@ class DPCodeWrapper(DeviceWrapper):
self.dpcode = dpcode
def skip_update(
self, device: CustomerDevice, updated_status_properties: list[str] | None
self,
device: CustomerDevice,
updated_status_properties: list[str] | None,
dp_timestamps: dict[str, int] | None,
) -> bool:
"""Determine if the wrapper should skip an update.

View File

@@ -554,10 +554,12 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
return
self.async_write_ha_state()

View File

@@ -410,10 +410,12 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
return
self.async_write_ha_state()

View File

@@ -1853,9 +1853,11 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
return
self.async_write_ha_state()

View File

@@ -110,10 +110,12 @@ class TuyaSirenEntity(TuyaEntity, SirenEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
return
self.async_write_ha_state()

View File

@@ -1043,10 +1043,12 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
return
self.async_write_ha_state()

View File

@@ -140,10 +140,12 @@ class TuyaValveEntity(TuyaEntity, ValveEntity):
async def _handle_state_update(
self,
updated_status_properties: list[str] | None,
dp_timestamps: dict | None = None,
dp_timestamps: dict[str, int] | None,
) -> None:
"""Handle state update, only if this entity's dpcode was actually updated."""
if self._dpcode_wrapper.skip_update(self.device, updated_status_properties):
if self._dpcode_wrapper.skip_update(
self.device, updated_status_properties, dp_timestamps
):
return
self.async_write_ha_state()

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["waterfurnace"],
"quality_scale": "legacy",
"requirements": ["waterfurnace==1.2.0"]
"requirements": ["waterfurnace==1.4.0"]
}

View File

@@ -47,6 +47,9 @@ async def async_setup_entry(
class WyomingTtsProvider(tts.TextToSpeechEntity):
"""Wyoming text-to-speech provider."""
_attr_default_options = {}
_attr_supported_options = [tts.ATTR_AUDIO_OUTPUT, tts.ATTR_VOICE, ATTR_SPEAKER]
def __init__(
self,
config_entry: ConfigEntry,
@@ -78,38 +81,13 @@ class WyomingTtsProvider(tts.TextToSpeechEntity):
self._voices[language], key=lambda v: v.name
)
self._supported_languages: list[str] = list(voice_languages)
self._attr_supported_languages = list(voice_languages)
if self._attr_supported_languages:
self._attr_default_language = self._attr_supported_languages[0]
self._attr_name = self._tts_service.name
self._attr_unique_id = f"{config_entry.entry_id}-tts"
@property
def default_language(self):
"""Return default language."""
if not self._supported_languages:
return None
return self._supported_languages[0]
@property
def supported_languages(self):
"""Return list of supported languages."""
return self._supported_languages
@property
def supported_options(self):
"""Return list of supported options like voice, emotion."""
return [
tts.ATTR_AUDIO_OUTPUT,
tts.ATTR_VOICE,
ATTR_SPEAKER,
]
@property
def default_options(self):
"""Return a dict include default options."""
return {}
@callback
def async_get_supported_voices(self, language: str) -> list[tts.Voice] | None:
"""Return a list of supported voices for a language."""

View File

@@ -294,6 +294,7 @@ FLOWS = {
"homekit",
"homekit_controller",
"homematicip_cloud",
"homevolt",
"homewizard",
"homeworks",
"honeywell",

View File

@@ -2836,6 +2836,12 @@
"zwave"
]
},
"homevolt": {
"name": "Homevolt",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"homewizard": {
"name": "HomeWizard",
"integration_type": "device",

View File

@@ -36,7 +36,7 @@ fnv-hash-fast==1.6.0
go2rtc-client==0.4.0
ha-ffmpeg==3.2.2
habluetooth==5.8.0
hass-nabucasa==1.10.0
hass-nabucasa==1.11.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260107.2

View File

@@ -85,6 +85,9 @@
"timeout_connect": "Timeout establishing connection",
"unknown": "Unexpected error"
},
"initiate_flow": {
"account": "Add account"
},
"title": {
"oauth2_pick_implementation": "Pick authentication method",
"reauth": "Authentication expired for {name}",

View File

@@ -103,6 +103,7 @@ _AMBIENT_IDEAL_GAS_MOLAR_VOLUME = ( # m3⋅mol⁻¹
)
# Molar masses in g⋅mol⁻¹
_CARBON_MONOXIDE_MOLAR_MASS = 28.01
_SULPHUR_DIOXIDE_MOLAR_MASS = 64.066
class BaseUnitConverter:
@@ -193,6 +194,7 @@ class CarbonMonoxideConcentrationConverter(BaseUnitConverter):
UNIT_CLASS = "carbon_monoxide"
_UNIT_CONVERSION: dict[str | None, float] = {
CONCENTRATION_PARTS_PER_BILLION: 1e9,
CONCENTRATION_PARTS_PER_MILLION: 1e6,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: (
_CARBON_MONOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e3
@@ -202,12 +204,29 @@ class CarbonMonoxideConcentrationConverter(BaseUnitConverter):
),
}
VALID_UNITS = {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
}
class SulphurDioxideConcentrationConverter(BaseUnitConverter):
"""Convert sulphur dioxide ratio to mass per volume."""
UNIT_CLASS = "sulphur_dioxide"
_UNIT_CONVERSION: dict[str | None, float] = {
CONCENTRATION_PARTS_PER_BILLION: 1e9,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: (
_SULPHUR_DIOXIDE_MOLAR_MASS / _AMBIENT_IDEAL_GAS_MOLAR_VOLUME * 1e6
),
}
VALID_UNITS = {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
}
class DataRateConverter(BaseUnitConverter):
"""Utility to convert data rate values."""

View File

@@ -168,7 +168,6 @@ _TEST_FIXTURES: dict[str, list[str] | str] = {
"service_calls": "list[ServiceCall]",
"snapshot": "SnapshotAssertion",
"socket_enabled": "None",
"stub_blueprint_populate": "None",
"tmp_path": "Path",
"tmpdir": "py.path.local",
"tts_mutagen_mock": "MagicMock",

View File

@@ -48,7 +48,7 @@ dependencies = [
"fnv-hash-fast==1.6.0",
# hass-nabucasa is imported by helpers which don't depend on the cloud
# integration
"hass-nabucasa==1.10.0",
"hass-nabucasa==1.11.0",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1",

2
requirements.txt generated
View File

@@ -24,7 +24,7 @@ cronsim==2.7
cryptography==46.0.2
fnv-hash-fast==1.6.0
ha-ffmpeg==3.2.2
hass-nabucasa==1.10.0
hass-nabucasa==1.11.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-intents==2026.1.6

11
requirements_all.txt generated
View File

@@ -1171,7 +1171,7 @@ habluetooth==5.8.0
hanna-cloud==0.0.7
# homeassistant.components.cloud
hass-nabucasa==1.10.0
hass-nabucasa==1.11.0
# homeassistant.components.splunk
hass-splunk==0.1.1
@@ -1226,6 +1226,9 @@ homelink-integration-api==0.0.1
# homeassistant.components.homematicip_cloud
homematicip==2.4.0
# homeassistant.components.homevolt
homevolt==0.2.4
# homeassistant.components.horizon
horimote==0.4.1
@@ -2045,7 +2048,7 @@ pyfibaro==0.8.3
pyfido==2.1.2
# homeassistant.components.firefly_iii
pyfirefly==0.1.11
pyfirefly==0.1.12
# homeassistant.components.fireservicerota
pyfireservicerota==0.0.46
@@ -2377,7 +2380,7 @@ pysabnzbd==1.1.1
pysaj==0.0.16
# homeassistant.components.saunum
pysaunum==0.2.0
pysaunum==0.3.0
# homeassistant.components.schlage
pyschlage==2025.9.0
@@ -3170,7 +3173,7 @@ wallbox==0.9.0
watchdog==6.0.0
# homeassistant.components.waterfurnace
waterfurnace==1.2.0
waterfurnace==1.4.0
# homeassistant.components.watergate
watergate-local-api==2025.1.0

View File

@@ -1041,7 +1041,7 @@ habluetooth==5.8.0
hanna-cloud==0.0.7
# homeassistant.components.cloud
hass-nabucasa==1.10.0
hass-nabucasa==1.11.0
# homeassistant.components.assist_satellite
# homeassistant.components.conversation
@@ -1084,6 +1084,9 @@ homelink-integration-api==0.0.1
# homeassistant.components.homematicip_cloud
homematicip==2.4.0
# homeassistant.components.homevolt
homevolt==0.2.4
# homeassistant.components.remember_the_milk
httplib2==0.20.4
@@ -1737,7 +1740,7 @@ pyfibaro==0.8.3
pyfido==2.1.2
# homeassistant.components.firefly_iii
pyfirefly==0.1.11
pyfirefly==0.1.12
# homeassistant.components.fireservicerota
pyfireservicerota==0.0.46
@@ -2012,7 +2015,7 @@ pyrympro==0.0.9
pysabnzbd==1.1.1
# homeassistant.components.saunum
pysaunum==0.2.0
pysaunum==0.3.0
# homeassistant.components.schlage
pyschlage==2025.9.0

View File

@@ -214,6 +214,10 @@ def gen_data_entry_schema(
vol.Required("user"): translation_value_validator,
str: translation_value_validator,
}
else:
schema[vol.Optional("initiate_flow")] = {
vol.Required("user"): translation_value_validator,
}
if flow_title == REQUIRED:
schema[vol.Required("title")] = translation_value_validator
elif flow_title == REMOVED:

View File

@@ -100,6 +100,13 @@ async def target_entities(
suggested_object_id=f"device_{domain}",
device_id=device.id,
)
entity_reg.async_get_or_create(
domain=domain,
platform="test",
unique_id=f"{domain}_device2",
suggested_object_id=f"device2_{domain}",
device_id=device.id,
)
entity_reg.async_get_or_create(
domain=domain,
platform="test",
@@ -130,9 +137,11 @@ async def target_entities(
return {
"included": [
f"{domain}.standalone_{domain}",
f"{domain}.standalone2_{domain}",
f"{domain}.label_{domain}",
f"{domain}.area_{domain}",
f"{domain}.device_{domain}",
f"{domain}.device2_{domain}",
],
"excluded": [
f"{domain}.standalone_{domain}_excluded",
@@ -150,17 +159,22 @@ def parametrize_target_entities(domain: str) -> list[tuple[dict, str, int]]:
"""
return [
(
{CONF_ENTITY_ID: f"{domain}.standalone_{domain}"},
{
CONF_ENTITY_ID: [
f"{domain}.standalone_{domain}",
f"{domain}.standalone2_{domain}",
]
},
f"{domain}.standalone_{domain}",
1,
2,
),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 2),
({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 2),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 2),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 2),
({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 2),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 2),
({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 1),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.label_{domain}", 3),
({ATTR_AREA_ID: "test_area"}, f"{domain}.area_{domain}", 3),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.area_{domain}", 3),
({ATTR_LABEL_ID: "test_label"}, f"{domain}.device_{domain}", 3),
({ATTR_AREA_ID: "test_area"}, f"{domain}.device_{domain}", 3),
({ATTR_FLOOR_ID: "test_floor"}, f"{domain}.device_{domain}", 3),
({ATTR_DEVICE_ID: "test_device"}, f"{domain}.device_{domain}", 2),
]
@@ -184,18 +198,19 @@ class ConditionStateDescription(TypedDict):
included: _StateDescription # State for entities meant to be targeted
excluded: _StateDescription # State for entities not meant to be targeted
state_valid: bool # False if the state of the included entities is missing (None), unavailable or unknown
condition_true: bool # If the condition is expected to evaluate to true
condition_true_first_entity: bool # If the condition is expected to evaluate to true for the first targeted entity
def parametrize_condition_states(
def _parametrize_condition_states(
*,
condition: str,
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
additional_attributes: dict | None = None,
additional_attributes: dict | None,
condition_true_if_invalid: bool,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
@@ -212,7 +227,7 @@ def parametrize_condition_states(
def state_with_attributes(
state: str | None | tuple[str | None, dict],
condition_true: bool,
state_valid: bool,
condition_true_first_entity: bool,
) -> ConditionStateDescription:
"""Return ConditionStateDescription dict."""
if isinstance(state, str) or state is None:
@@ -226,7 +241,7 @@ def parametrize_condition_states(
"attributes": {},
},
"condition_true": condition_true,
"state_valid": state_valid,
"condition_true_first_entity": condition_true_first_entity,
}
return {
"included": {
@@ -238,7 +253,7 @@ def parametrize_condition_states(
"attributes": state[1],
},
"condition_true": condition_true,
"state_valid": state_valid,
"condition_true_first_entity": condition_true_first_entity,
}
return [
@@ -247,11 +262,19 @@ def parametrize_condition_states(
condition_options,
list(
itertools.chain(
(state_with_attributes(None, False, False),),
(state_with_attributes(STATE_UNAVAILABLE, False, False),),
(state_with_attributes(STATE_UNKNOWN, False, False),),
(state_with_attributes(None, condition_true_if_invalid, True),),
(
state_with_attributes(other_state, False, True)
state_with_attributes(
STATE_UNAVAILABLE, condition_true_if_invalid, True
),
),
(
state_with_attributes(
STATE_UNKNOWN, condition_true_if_invalid, True
),
),
(
state_with_attributes(other_state, False, False)
for other_state in other_states
),
),
@@ -263,8 +286,8 @@ def parametrize_condition_states(
condition,
condition_options,
[
state_with_attributes(other_states[0], False, True),
state_with_attributes(target_state, True, True),
state_with_attributes(other_states[0], False, False),
state_with_attributes(target_state, True, False),
],
)
for target_state in target_states
@@ -272,6 +295,60 @@ def parametrize_condition_states(
]
def parametrize_condition_states_any(
*,
condition: str,
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
additional_attributes: dict | None = None,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
The target_states and other_states iterables are either iterables of
states or iterables of (state, attributes) tuples.
Returns a list of tuples with (condition, condition options, list of states),
where states is a list of ConditionStateDescription dicts.
"""
return _parametrize_condition_states(
condition=condition,
condition_options=condition_options,
target_states=target_states,
other_states=other_states,
additional_attributes=additional_attributes,
condition_true_if_invalid=False,
)
def parametrize_condition_states_all(
*,
condition: str,
condition_options: dict[str, Any] | None = None,
target_states: list[str | None | tuple[str | None, dict]],
other_states: list[str | None | tuple[str | None, dict]],
additional_attributes: dict | None = None,
) -> list[tuple[str, dict[str, Any], list[ConditionStateDescription]]]:
"""Parametrize states and expected condition evaluations.
The target_states and other_states iterables are either iterables of
states or iterables of (state, attributes) tuples.
Returns a list of tuples with (condition, condition options, list of states),
where states is a list of ConditionStateDescription dicts.
"""
return _parametrize_condition_states(
condition=condition,
condition_options=condition_options,
target_states=target_states,
other_states=other_states,
additional_attributes=additional_attributes,
condition_true_if_invalid=True,
)
def parametrize_trigger_states(
*,
trigger: str,

View File

@@ -16,18 +16,14 @@ from tests.components import (
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_alarm_control_panels(hass: HomeAssistant) -> list[str]:
"""Create multiple alarm_control_panel entities associated with different targets."""
@@ -61,7 +57,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="alarm_control_panel.is_armed",
target_states=[
AlarmControlPanelState.ARMED_AWAY,
@@ -78,7 +74,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
AlarmControlPanelState.TRIGGERED,
],
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="alarm_control_panel.is_armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
@@ -86,7 +82,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="alarm_control_panel.is_armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
@@ -94,7 +90,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="alarm_control_panel.is_armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
@@ -102,7 +98,7 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="alarm_control_panel.is_armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
@@ -110,12 +106,12 @@ async def test_alarm_control_panel_conditions_gated_by_labs_flag(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="alarm_control_panel.is_disarmed",
target_states=[AlarmControlPanelState.DISARMED],
other_states=other_states(AlarmControlPanelState.DISARMED),
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="alarm_control_panel.is_triggered",
target_states=[AlarmControlPanelState.TRIGGERED],
other_states=other_states(AlarmControlPanelState.TRIGGERED),
@@ -168,7 +164,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="alarm_control_panel.is_armed",
target_states=[
AlarmControlPanelState.ARMED_AWAY,
@@ -185,7 +181,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
AlarmControlPanelState.TRIGGERED,
],
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="alarm_control_panel.is_armed_away",
target_states=[AlarmControlPanelState.ARMED_AWAY],
other_states=other_states(AlarmControlPanelState.ARMED_AWAY),
@@ -193,7 +189,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_AWAY
},
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="alarm_control_panel.is_armed_home",
target_states=[AlarmControlPanelState.ARMED_HOME],
other_states=other_states(AlarmControlPanelState.ARMED_HOME),
@@ -201,7 +197,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_HOME
},
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="alarm_control_panel.is_armed_night",
target_states=[AlarmControlPanelState.ARMED_NIGHT],
other_states=other_states(AlarmControlPanelState.ARMED_NIGHT),
@@ -209,7 +205,7 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_NIGHT
},
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="alarm_control_panel.is_armed_vacation",
target_states=[AlarmControlPanelState.ARMED_VACATION],
other_states=other_states(AlarmControlPanelState.ARMED_VACATION),
@@ -217,12 +213,12 @@ async def test_alarm_control_panel_state_condition_behavior_any(
ATTR_SUPPORTED_FEATURES: AlarmControlPanelEntityFeature.ARM_VACATION
},
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="alarm_control_panel.is_disarmed",
target_states=[AlarmControlPanelState.DISARMED],
other_states=other_states(AlarmControlPanelState.DISARMED),
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="alarm_control_panel.is_triggered",
target_states=[AlarmControlPanelState.TRIGGERED],
other_states=other_states(AlarmControlPanelState.TRIGGERED),
@@ -259,17 +255,10 @@ async def test_alarm_control_panel_state_condition_behavior_all(
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"])
or (state["condition_true"] and entities_in_target == 1)
)
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"]) or state["condition_true"]
)
assert condition(hass) == state["condition_true"]

View File

@@ -25,11 +25,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_action_types"),
[

View File

@@ -18,11 +18,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_condition_types"),
[

View File

@@ -26,11 +26,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_trigger_types"),
[

View File

@@ -22,11 +22,6 @@ from tests.components import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_alarm_control_panels(hass: HomeAssistant) -> list[str]:
"""Create multiple alarm control panel entities associated with different targets."""

View File

@@ -1,7 +1,5 @@
"""The tests for Arcam FMJ Receiver control device triggers."""
import pytest
from homeassistant.components import automation
from homeassistant.components.arcam_fmj.const import DOMAIN
from homeassistant.components.device_automation import DeviceAutomationType
@@ -12,11 +10,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_triggers(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -12,18 +12,14 @@ from tests.components import (
assert_condition_gated_by_labs_flag,
create_target_condition,
other_states,
parametrize_condition_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_assist_satellites(hass: HomeAssistant) -> list[str]:
"""Create multiple assist satellite entities associated with different targets."""
@@ -54,22 +50,22 @@ async def test_assist_satellite_conditions_gated_by_labs_flag(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="assist_satellite.is_idle",
target_states=[AssistSatelliteState.IDLE],
other_states=other_states(AssistSatelliteState.IDLE),
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="assist_satellite.is_listening",
target_states=[AssistSatelliteState.LISTENING],
other_states=other_states(AssistSatelliteState.LISTENING),
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="assist_satellite.is_processing",
target_states=[AssistSatelliteState.PROCESSING],
other_states=other_states(AssistSatelliteState.PROCESSING),
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="assist_satellite.is_responding",
target_states=[AssistSatelliteState.RESPONDING],
other_states=other_states(AssistSatelliteState.RESPONDING),
@@ -122,22 +118,22 @@ async def test_assist_satellite_state_condition_behavior_any(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="assist_satellite.is_idle",
target_states=[AssistSatelliteState.IDLE],
other_states=other_states(AssistSatelliteState.IDLE),
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="assist_satellite.is_listening",
target_states=[AssistSatelliteState.LISTENING],
other_states=other_states(AssistSatelliteState.LISTENING),
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="assist_satellite.is_processing",
target_states=[AssistSatelliteState.PROCESSING],
other_states=other_states(AssistSatelliteState.PROCESSING),
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="assist_satellite.is_responding",
target_states=[AssistSatelliteState.RESPONDING],
other_states=other_states(AssistSatelliteState.RESPONDING),
@@ -174,17 +170,10 @@ async def test_assist_satellite_state_condition_behavior_all(
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"])
or (state["condition_true"] and entities_in_target == 1)
)
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"]) or state["condition_true"]
)
assert condition(hass) == state["condition_true"]

View File

@@ -19,11 +19,6 @@ from tests.components import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_assist_satellites(hass: HomeAssistant) -> list[str]:
"""Create multiple assist satellite entities associated with different targets."""

View File

@@ -1,8 +0,0 @@
"""Conftest for automation tests."""
import pytest
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""

View File

@@ -26,11 +26,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_conditions(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -26,11 +26,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_triggers(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -23,11 +23,6 @@ from tests.components import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_binary_sensors(hass: HomeAssistant) -> tuple[list[str], list[str]]:
"""Create multiple binary sensor entities associated with different targets."""

View File

@@ -1,12 +0,0 @@
"""Blueprints test helpers."""
from collections.abc import Generator
from unittest.mock import patch
def stub_blueprint_populate_fixture_helper() -> Generator[None]:
"""Stub copying the blueprints to the config folder."""
with patch(
"homeassistant.components.blueprint.models.DomainBlueprints.async_populate"
):
yield

View File

@@ -1,8 +0,0 @@
"""Blueprints conftest."""
import pytest
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""

View File

@@ -19,11 +19,6 @@ from tests.components import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_buttons(hass: HomeAssistant) -> list[str]:
"""Create multiple button entities associated with different targets."""

View File

@@ -24,11 +24,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_action_types"),
[

View File

@@ -20,11 +20,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_condition_types"),
[

View File

@@ -26,11 +26,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_triggers(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -38,11 +38,6 @@ from tests.components import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_climates(hass: HomeAssistant) -> list[str]:
"""Create multiple climate entities associated with different targets."""

View File

@@ -18,16 +18,10 @@ from homeassistant.util import yaml as yaml_util
from tests.typing import ClientSessionGenerator
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def setup_automation(
hass: HomeAssistant,
automation_config: dict[str, Any],
stub_blueprint_populate: None,
) -> None:
"""Set up automation integration."""
assert await async_setup_component(

View File

@@ -17,11 +17,6 @@ from tests.common import MockConfigEntry, MockModule, mock_integration
from tests.typing import MockHAClientWebSocket, WebSocketGenerator
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture(name="client")
async def client_fixture(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator

View File

@@ -18,11 +18,6 @@ from homeassistant.util import yaml as yaml_util
from tests.typing import ClientSessionGenerator
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture(autouse=True)
async def setup_script(hass: HomeAssistant, script_config: dict[str, Any]) -> None:
"""Set up script integration."""

View File

@@ -108,17 +108,6 @@ def entity_registry_enabled_by_default() -> Generator[None]:
yield
# Blueprint test fixtures
@pytest.fixture(name="stub_blueprint_populate")
def stub_blueprint_populate_fixture() -> Generator[None]:
"""Stub copying the blueprints to the config folder."""
from .blueprint.common import ( # noqa: PLC0415
stub_blueprint_populate_fixture_helper,
)
yield from stub_blueprint_populate_fixture_helper()
# TTS test fixtures
@pytest.fixture(name="mock_tts_get_cache_files")
def mock_tts_get_cache_files_fixture() -> Generator[MagicMock]:

View File

@@ -23,11 +23,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_action_types"),
[

View File

@@ -22,11 +22,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_condition_types"),
[

View File

@@ -26,11 +26,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
("set_state", "features_reg", "features_state", "expected_trigger_types"),
[

View File

@@ -39,11 +39,6 @@ from .conftest import WebsocketDataType
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.mark.parametrize(
"sensor_payload",
[

View File

@@ -10,11 +10,6 @@ from homeassistant.helpers import recorder as recorder_helper
from homeassistant.setup import async_setup_component
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture(autouse=True)
def mock_ssdp():
"""Mock ssdp."""

View File

@@ -10,11 +10,6 @@ from homeassistant.setup import async_setup_component
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture(autouse=True)
async def setup_homeassistant(hass: HomeAssistant):
"""Set up the homeassistant integration."""

View File

@@ -34,11 +34,6 @@ class MockDeviceEntry(dr.DeviceEntry):
id: str = attr.ib(default="very_unique")
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
def fake_integration(hass: HomeAssistant) -> None:
"""Set up a mock integration with device automation support."""

View File

@@ -14,11 +14,6 @@ from homeassistant.util import dt as dt_util
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_if_fires_on_state_change(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -15,11 +15,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_conditions(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -19,12 +19,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
AWAY_LATITUDE = 32.881011
AWAY_LONGITUDE = -117.234758

View File

@@ -24,11 +24,6 @@ from tests.components import (
STATE_WORK_ZONE = "work"
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_device_trackers(hass: HomeAssistant) -> list[str]:
"""Create multiple device_trackers entities associated with different targets."""

View File

@@ -1,8 +0,0 @@
"""Conftest for emulated_hue tests."""
import pytest
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""

View File

@@ -11,18 +11,14 @@ from tests.components import (
ConditionStateDescription,
assert_condition_gated_by_labs_flag,
create_target_condition,
parametrize_condition_states,
parametrize_condition_states_all,
parametrize_condition_states_any,
parametrize_target_entities,
set_or_remove_state,
target_entities,
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_fans(hass: HomeAssistant) -> list[str]:
"""Create multiple fan entities associated with different targets."""
@@ -57,12 +53,12 @@ async def test_fan_conditions_gated_by_labs_flag(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="fan.is_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
*parametrize_condition_states(
*parametrize_condition_states_any(
condition="fan.is_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
@@ -123,12 +119,12 @@ async def test_fan_state_condition_behavior_any(
@pytest.mark.parametrize(
("condition", "condition_options", "states"),
[
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="fan.is_on",
target_states=[STATE_ON],
other_states=[STATE_OFF],
),
*parametrize_condition_states(
*parametrize_condition_states_all(
condition="fan.is_off",
target_states=[STATE_OFF],
other_states=[STATE_ON],
@@ -169,17 +165,10 @@ async def test_fan_state_condition_behavior_all(
set_or_remove_state(hass, entity_id, included_state)
await hass.async_block_till_done()
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"])
or (state["condition_true"] and entities_in_target == 1)
)
assert condition(hass) == state["condition_true_first_entity"]
for other_entity_id in other_entity_ids:
set_or_remove_state(hass, other_entity_id, included_state)
await hass.async_block_till_done()
# The condition passes if all entities are either in a target state or invalid
assert condition(hass) == (
(not state["state_valid"]) or state["condition_true"]
)
assert condition(hass) == state["condition_true"]

View File

@@ -19,11 +19,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_actions(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -15,11 +15,6 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry, async_get_device_automations
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_conditions(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -23,11 +23,6 @@ from tests.common import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
async def test_get_triggers(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,

View File

@@ -17,11 +17,6 @@ from tests.components import (
)
@pytest.fixture(autouse=True, name="stub_blueprint_populate")
def stub_blueprint_populate_autouse(stub_blueprint_populate: None) -> None:
"""Stub copying the blueprints to the config folder."""
@pytest.fixture
async def target_fans(hass: HomeAssistant) -> list[str]:
"""Create multiple fan entities associated with different targets."""

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