Compare commits

..

1 Commits

Author SHA1 Message Date
Erik
5c0df09dc9 Remove template from sql service schema 2025-11-11 10:15:05 +01:00
50 changed files with 397 additions and 763 deletions

View File

@@ -9,5 +9,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["apyhiveapi"],
"requirements": ["pyhive-integration==1.0.7"]
"requirements": ["pyhive-integration==1.0.6"]
}

View File

@@ -13,7 +13,6 @@ from typing import Any
from aiohttp import web
from hyperion import client
from hyperion.const import (
KEY_DATA,
KEY_IMAGE,
KEY_IMAGE_STREAM,
KEY_LEDCOLORS,
@@ -156,8 +155,7 @@ class HyperionCamera(Camera):
"""Update Hyperion components."""
if not img:
return
# Prefer KEY_DATA (Hyperion server >= 2.1.1); fall back to KEY_RESULT for older server versions
img_data = img.get(KEY_DATA, img.get(KEY_RESULT, {})).get(KEY_IMAGE)
img_data = img.get(KEY_RESULT, {}).get(KEY_IMAGE)
if not img_data or not img_data.startswith(IMAGE_STREAM_JPG_SENTINEL):
return
async with self._image_cond:

View File

@@ -19,13 +19,11 @@ from homeassistant.core import (
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.trigger_template_entity import ValueTemplate
from homeassistant.util.json import JsonValueType
from .const import CONF_QUERY, DOMAIN
from .util import (
async_create_sessionmaker,
check_and_render_sql_query,
convert_value,
generate_lambda_stmt,
redact_credentials,
@@ -39,9 +37,7 @@ _LOGGER = logging.getLogger(__name__)
SERVICE_QUERY = "query"
SERVICE_QUERY_SCHEMA = vol.Schema(
{
vol.Required(CONF_QUERY): vol.All(
cv.template, ValueTemplate.from_template, validate_sql_select
),
vol.Required(CONF_QUERY): vol.All(cv.string, validate_sql_select),
vol.Optional(CONF_DB_URL): cv.string,
}
)
@@ -76,9 +72,8 @@ async def _async_query_service(
def _execute_and_convert_query() -> list[JsonValueType]:
"""Execute the query and return the results with converted types."""
sess: Session = sessmaker()
rendered_query = check_and_render_sql_query(call.hass, query_str)
try:
result: Result = sess.execute(generate_lambda_stmt(rendered_query))
result: Result = sess.execute(generate_lambda_stmt(query_str))
except SQLAlchemyError as err:
_LOGGER.debug(
"Error executing query %s: %s",

View File

@@ -18,7 +18,7 @@ import voluptuous as vol
from homeassistant.components.recorder import SupportedDialect, get_instance
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.core import Event, HomeAssistant, async_get_hass, callback
from homeassistant.exceptions import HomeAssistantError, TemplateError
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.template import Template
@@ -46,11 +46,15 @@ def resolve_db_url(hass: HomeAssistant, db_url: str | None) -> str:
return get_instance(hass).db_url
def validate_sql_select(value: Template) -> Template:
def validate_sql_select(value: Template | str) -> Template | str:
"""Validate that value is a SQL SELECT query."""
hass: HomeAssistant
if isinstance(value, str):
hass = async_get_hass()
else:
hass = value.hass # type: ignore[assignment]
try:
assert value.hass
check_and_render_sql_query(value.hass, value)
check_and_render_sql_query(hass, value)
except (TemplateError, InvalidSqlQuery) as err:
raise vol.Invalid(str(err)) from err
return value

View File

@@ -75,7 +75,6 @@ PLATFORMS_BY_TYPE = {
SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR],
SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.S10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.S20_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_PRO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_PRO_COMBO_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
@@ -103,10 +102,6 @@ PLATFORMS_BY_TYPE = {
SupportedModels.RELAY_SWITCH_2PM.value: [Platform.SWITCH, Platform.SENSOR],
SupportedModels.GARAGE_DOOR_OPENER.value: [Platform.COVER, Platform.SENSOR],
SupportedModels.CLIMATE_PANEL.value: [Platform.SENSOR, Platform.BINARY_SENSOR],
SupportedModels.SMART_THERMOSTAT_RADIATOR.value: [
Platform.CLIMATE,
Platform.SENSOR,
],
}
CLASS_BY_DEVICE = {
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
@@ -124,7 +119,6 @@ CLASS_BY_DEVICE = {
SupportedModels.ROLLER_SHADE.value: switchbot.SwitchbotRollerShade,
SupportedModels.CIRCULATOR_FAN.value: switchbot.SwitchbotFan,
SupportedModels.S10_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.S20_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.K10_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.K10_PRO_VACUUM.value: switchbot.SwitchbotVacuum,
SupportedModels.K10_PRO_COMBO_VACUUM.value: switchbot.SwitchbotVacuum,
@@ -142,7 +136,6 @@ CLASS_BY_DEVICE = {
SupportedModels.PLUG_MINI_EU.value: switchbot.SwitchbotRelaySwitch,
SupportedModels.RELAY_SWITCH_2PM.value: switchbot.SwitchbotRelaySwitch2PM,
SupportedModels.GARAGE_DOOR_OPENER.value: switchbot.SwitchbotGarageDoorOpener,
SupportedModels.SMART_THERMOSTAT_RADIATOR.value: switchbot.SwitchbotSmartThermostatRadiator,
}

View File

@@ -1,140 +0,0 @@
"""Support for Switchbot Climate devices."""
from __future__ import annotations
import logging
from typing import Any
import switchbot
from switchbot import (
ClimateAction as SwitchBotClimateAction,
ClimateMode as SwitchBotClimateMode,
)
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import SwitchbotConfigEntry
from .entity import SwitchbotEntity, exception_handler
SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE = {
SwitchBotClimateMode.HEAT: HVACMode.HEAT,
SwitchBotClimateMode.OFF: HVACMode.OFF,
}
HASS_HVAC_MODE_TO_SWITCHBOT_CLIMATE = {
HVACMode.HEAT: SwitchBotClimateMode.HEAT,
HVACMode.OFF: SwitchBotClimateMode.OFF,
}
SWITCHBOT_ACTION_TO_HASS_HVAC_ACTION = {
SwitchBotClimateAction.HEATING: HVACAction.HEATING,
SwitchBotClimateAction.IDLE: HVACAction.IDLE,
SwitchBotClimateAction.OFF: HVACAction.OFF,
}
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: SwitchbotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Switchbot climate based on a config entry."""
coordinator = entry.runtime_data
async_add_entities([SwitchBotClimateEntity(coordinator)])
class SwitchBotClimateEntity(SwitchbotEntity, ClimateEntity):
"""Representation of a Switchbot Climate device."""
_device: switchbot.SwitchbotDevice
_attr_supported_features = (
ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_target_temperature_step = 0.5
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "climate"
_attr_name = None
@property
def min_temp(self) -> float:
"""Return the minimum temperature."""
return self._device.min_temperature
@property
def max_temp(self) -> float:
"""Return the maximum temperature."""
return self._device.max_temperature
@property
def preset_modes(self) -> list[str] | None:
"""Return the list of available preset modes."""
return self._device.preset_modes
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
return self._device.preset_mode
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
return SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE.get(
self._device.hvac_mode, HVACMode.OFF
)
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return the list of available HVAC modes."""
return [
SWITCHBOT_CLIMATE_TO_HASS_HVAC_MODE[mode]
for mode in self._device.hvac_modes
]
@property
def hvac_action(self) -> HVACAction | None:
"""Return the current HVAC action."""
return SWITCHBOT_ACTION_TO_HASS_HVAC_ACTION.get(
self._device.hvac_action, HVACAction.OFF
)
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self._device.current_temperature
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return self._device.target_temperature
@exception_handler
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new HVAC mode."""
return await self._device.set_hvac_mode(
HASS_HVAC_MODE_TO_SWITCHBOT_CLIMATE[hvac_mode]
)
@exception_handler
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
return await self._device.set_preset_mode(preset_mode)
@exception_handler
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE)
return await self._device.set_target_temperature(temperature)

View File

@@ -58,8 +58,6 @@ class SupportedModels(StrEnum):
K11_PLUS_VACUUM = "k11+_vacuum"
GARAGE_DOOR_OPENER = "garage_door_opener"
CLIMATE_PANEL = "climate_panel"
SMART_THERMOSTAT_RADIATOR = "smart_thermostat_radiator"
S20_VACUUM = "s20_vacuum"
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
@@ -80,7 +78,6 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
SwitchbotModel.CIRCULATOR_FAN: SupportedModels.CIRCULATOR_FAN,
SwitchbotModel.K20_VACUUM: SupportedModels.K20_VACUUM,
SwitchbotModel.S10_VACUUM: SupportedModels.S10_VACUUM,
SwitchbotModel.S20_VACUUM: SupportedModels.S20_VACUUM,
SwitchbotModel.K10_VACUUM: SupportedModels.K10_VACUUM,
SwitchbotModel.K10_PRO_VACUUM: SupportedModels.K10_PRO_VACUUM,
SwitchbotModel.K10_PRO_COMBO_VACUUM: SupportedModels.K10_PRO_COMBO_VACUUM,
@@ -98,7 +95,6 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
SwitchbotModel.K11_VACUUM: SupportedModels.K11_PLUS_VACUUM,
SwitchbotModel.GARAGE_DOOR_OPENER: SupportedModels.GARAGE_DOOR_OPENER,
SwitchbotModel.CLIMATE_PANEL: SupportedModels.CLIMATE_PANEL,
SwitchbotModel.SMART_THERMOSTAT_RADIATOR: SupportedModels.SMART_THERMOSTAT_RADIATOR,
}
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
@@ -136,7 +132,6 @@ ENCRYPTED_MODELS = {
SwitchbotModel.PLUG_MINI_EU,
SwitchbotModel.RELAY_SWITCH_2PM,
SwitchbotModel.GARAGE_DOOR_OPENER,
SwitchbotModel.SMART_THERMOSTAT_RADIATOR,
}
ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
@@ -158,7 +153,6 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
SwitchbotModel.PLUG_MINI_EU: switchbot.SwitchbotRelaySwitch,
SwitchbotModel.RELAY_SWITCH_2PM: switchbot.SwitchbotRelaySwitch2PM,
SwitchbotModel.GARAGE_DOOR_OPENER: switchbot.SwitchbotRelaySwitch,
SwitchbotModel.SMART_THERMOSTAT_RADIATOR: switchbot.SwitchbotSmartThermostatRadiator,
}
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {

View File

@@ -1,18 +1,5 @@
{
"entity": {
"climate": {
"climate": {
"state_attributes": {
"preset_mode": {
"state": {
"manual": "mdi:hand-back-right",
"off": "mdi:hvac-off",
"schedule": "mdi:calendar-clock"
}
}
}
}
},
"fan": {
"air_purifier": {
"default": "mdi:air-purifier",

View File

@@ -100,19 +100,6 @@
"name": "Unlocked alarm"
}
},
"climate": {
"climate": {
"state_attributes": {
"preset_mode": {
"state": {
"manual": "[%key:common::state::manual%]",
"off": "[%key:common::state::off%]",
"schedule": "Schedule"
}
}
}
}
},
"cover": {
"cover": {
"state_attributes": {

View File

@@ -1,20 +1,17 @@
"""Support for VELUX KLF 200 devices."""
from __future__ import annotations
from pyvlx import PyVLX, PyVLXException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr
from .const import DOMAIN, LOGGER, PLATFORMS
type VeluxConfigEntry = ConfigEntry[PyVLX]
async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the velux component."""
host = entry.data[CONF_HOST]
password = entry.data[CONF_PASSWORD]
@@ -30,21 +27,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> boo
entry.runtime_data = pyvlx
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, f"gateway_{entry.entry_id}")},
name="KLF 200 Gateway",
manufacturer="Velux",
model="KLF 200",
hw_version=(
str(pyvlx.klf200.version.hardwareversion) if pyvlx.klf200.version else None
),
sw_version=(
str(pyvlx.klf200.version.softwareversion) if pyvlx.klf200.version else None
),
)
async def on_hass_stop(event):
"""Close connection when hass stops."""
LOGGER.debug("Velux interface terminated")
@@ -64,6 +46,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> boo
return True
async def async_unload_entry(hass: HomeAssistant, entry: VeluxConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -24,14 +24,14 @@ SCAN_INTERVAL = timedelta(minutes=5) # Use standard polling
async def async_setup_entry(
hass: HomeAssistant,
config_entry: VeluxConfigEntry,
config: VeluxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up rain sensor(s) for Velux platform."""
pyvlx = config_entry.runtime_data
pyvlx = config.runtime_data
async_add_entities(
VeluxRainSensor(node, config_entry.entry_id)
VeluxRainSensor(node, config.entry_id)
for node in pyvlx.nodes
if isinstance(node, Window) and node.rain_sensor
)

View File

@@ -32,13 +32,13 @@ PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: VeluxConfigEntry,
config: VeluxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up cover(s) for Velux platform."""
pyvlx = config_entry.runtime_data
pyvlx = config.runtime_data
async_add_entities(
VeluxCover(node, config_entry.entry_id)
VeluxCover(node, config.entry_id)
for node in pyvlx.nodes
if isinstance(node, OpeningDevice)
)

View File

@@ -18,23 +18,22 @@ class VeluxEntity(Entity):
def __init__(self, node: Node, config_entry_id: str) -> None:
"""Initialize the Velux device."""
self.node = node
unique_id = (
self._attr_unique_id = (
node.serial_number
if node.serial_number
else f"{config_entry_id}_{node.node_id}"
)
self._attr_unique_id = unique_id
self._attr_device_info = DeviceInfo(
identifiers={
(
DOMAIN,
unique_id,
node.serial_number
if node.serial_number
else f"{config_entry_id}_{node.node_id}",
)
},
name=node.name if node.name else f"#{node.node_id}",
serial_number=node.serial_number,
via_device=(DOMAIN, f"gateway_{config_entry_id}"),
)
@callback

View File

@@ -18,13 +18,13 @@ PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: VeluxConfigEntry,
config: VeluxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up light(s) for Velux platform."""
pyvlx = config_entry.runtime_data
pyvlx = config.runtime_data
async_add_entities(
VeluxLight(node, config_entry.entry_id)
VeluxLight(node, config.entry_id)
for node in pyvlx.nodes
if isinstance(node, LighteningDevice)
)

View File

@@ -15,11 +15,11 @@ PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
config_entry: VeluxConfigEntry,
config: VeluxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the scenes for Velux platform."""
pyvlx = config_entry.runtime_data
pyvlx = config.runtime_data
entities = [VeluxScene(scene) for scene in pyvlx.scenes]
async_add_entities(entities)

View File

@@ -58,7 +58,6 @@ from .utils import (
get_compressors,
get_device_serial,
is_supported,
normalize_state,
)
_LOGGER = logging.getLogger(__name__)
@@ -1087,7 +1086,7 @@ COMPRESSOR_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
ViCareSensorEntityDescription(
key="compressor_phase",
translation_key="compressor_phase",
value_getter=lambda api: normalize_state(api.getPhase()),
value_getter=lambda api: api.getPhase(),
entity_category=EntityCategory.DIAGNOSTIC,
),
)

View File

@@ -213,18 +213,7 @@
"name": "Compressor hours load class 5"
},
"compressor_phase": {
"name": "Compressor phase",
"state": {
"cooling": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::cooling%]",
"defrost": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::defrosting%]",
"heating": "[%key:component::climate::entity_component::_::state_attributes::hvac_action::state::heating%]",
"off": "[%key:common::state::off%]",
"passive_defrost": "Passive defrosting",
"pause": "[%key:common::state::idle%]",
"preparing": "Preparing",
"preparing_defrost": "Preparing defrost",
"ready": "[%key:common::state::idle%]"
}
"name": "Compressor phase"
},
"compressor_starts": {
"name": "Compressor starts"

View File

@@ -133,8 +133,3 @@ def get_compressors(device: PyViCareDevice) -> list[PyViCareHeatingDeviceCompone
def filter_state(state: str) -> str | None:
"""Return the state if not 'nothing' or 'unknown'."""
return None if state in ("nothing", "unknown") else state
def normalize_state(state: str) -> str:
"""Return the state with underscores instead of hyphens."""
return state.replace("-", "_")

View File

@@ -76,12 +76,12 @@ class EventAreaRegistryUpdatedData(TypedDict):
class AreaEntry(NormalizedNameBaseRegistryEntry):
"""Area Registry Entry."""
aliases: frozenset[str]
aliases: set[str]
floor_id: str | None
humidity_entity_id: str | None
icon: str | None
id: str
labels: frozenset[str] = field(default_factory=frozenset)
labels: set[str] = field(default_factory=set)
picture: str | None
temperature_entity_id: str | None
_cache: dict[str, Any] = field(default_factory=dict, compare=False, init=False)
@@ -295,12 +295,12 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
_validate_temperature_entity(self.hass, temperature_entity_id)
area = AreaEntry(
aliases=frozenset(aliases) if aliases else frozenset(),
aliases=aliases or set(),
floor_id=floor_id,
humidity_entity_id=humidity_entity_id,
icon=icon,
id=self._generate_id(name),
labels=frozenset(labels) if labels else frozenset(),
labels=labels or set(),
name=name,
picture=picture,
temperature_entity_id=temperature_entity_id,
@@ -338,11 +338,11 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
self,
area_id: str,
*,
aliases: frozenset[str] | set[str] | UndefinedType = UNDEFINED,
aliases: set[str] | UndefinedType = UNDEFINED,
floor_id: str | None | UndefinedType = UNDEFINED,
humidity_entity_id: str | None | UndefinedType = UNDEFINED,
icon: str | None | UndefinedType = UNDEFINED,
labels: frozenset[str] | set[str] | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
name: str | UndefinedType = UNDEFINED,
picture: str | None | UndefinedType = UNDEFINED,
temperature_entity_id: str | None | UndefinedType = UNDEFINED,
@@ -374,11 +374,11 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
self,
area_id: str,
*,
aliases: frozenset[str] | set[str] | UndefinedType = UNDEFINED,
aliases: set[str] | UndefinedType = UNDEFINED,
floor_id: str | None | UndefinedType = UNDEFINED,
humidity_entity_id: str | None | UndefinedType = UNDEFINED,
icon: str | None | UndefinedType = UNDEFINED,
labels: frozenset[str] | set[str] | UndefinedType = UNDEFINED,
labels: set[str] | UndefinedType = UNDEFINED,
name: str | UndefinedType = UNDEFINED,
picture: str | None | UndefinedType = UNDEFINED,
temperature_entity_id: str | None | UndefinedType = UNDEFINED,
@@ -389,23 +389,17 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
new_values: dict[str, Any] = {
attr_name: value
for attr_name, value in (
("aliases", aliases),
("floor_id", floor_id),
("humidity_entity_id", humidity_entity_id),
("icon", icon),
("labels", labels),
("picture", picture),
("temperature_entity_id", temperature_entity_id),
)
if value is not UNDEFINED and value != getattr(old, attr_name)
}
for attr_name, value in (
("aliases", aliases),
("labels", labels),
):
if value is UNDEFINED or value == getattr(old, attr_name):
continue
new_values[attr_name] = frozenset(value)
if "humidity_entity_id" in new_values and humidity_entity_id is not None:
_validate_humidity_entity(self.hass, new_values["humidity_entity_id"])
@@ -438,12 +432,12 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]):
for area in data["areas"]:
assert area["name"] is not None and area["id"] is not None
areas[area["id"]] = AreaEntry(
aliases=frozenset(area["aliases"]),
aliases=set(area["aliases"]),
floor_id=area["floor_id"],
humidity_entity_id=area["humidity_entity_id"],
icon=area["icon"],
id=area["id"],
labels=frozenset(area["labels"]),
labels=set(area["labels"]),
name=area["name"],
picture=area["picture"],
temperature_entity_id=area["temperature_entity_id"],

2
requirements_all.txt generated
View File

@@ -2050,7 +2050,7 @@ pyhaversion==22.8.0
pyheos==1.0.6
# homeassistant.components.hive
pyhive-integration==1.0.7
pyhive-integration==1.0.6
# homeassistant.components.homematic
pyhomematic==0.1.77

View File

@@ -32,7 +32,7 @@ pytest-timeout==2.4.0
pytest-unordered==0.7.0
pytest-picked==0.5.1
pytest-xdist==3.8.0
pytest==9.0.0
pytest==8.4.2
requests-mock==1.12.1
respx==0.22.0
syrupy==5.0.0

View File

@@ -1709,7 +1709,7 @@ pyhaversion==22.8.0
pyheos==1.0.6
# homeassistant.components.hive
pyhive-integration==1.0.7
pyhive-integration==1.0.6
# homeassistant.components.homematic
pyhomematic==0.1.77

View File

@@ -6,11 +6,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from . import api
@@ -28,13 +26,17 @@ type New_NameConfigEntry = ConfigEntry[api.AsyncConfigEntryAuth]
async def async_setup_entry(hass: HomeAssistant, entry: New_NameConfigEntry) -> bool:
"""Set up NEW_NAME from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
"OAuth2 implementation temporarily unavailable, will retry"
) from err
session = OAuth2Session(hass, entry, implementation)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
# If using a requests-based API lib
entry.runtime_data = api.ConfigEntryAuth(hass, session)

View File

@@ -1,5 +1,5 @@
# serializer version: 1
# name: test_alarm_control_panel[amax_3000-None][alarm_control_panel.area1-entry]
# name: test_alarm_control_panel[None-amax_3000][alarm_control_panel.area1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -34,7 +34,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_alarm_control_panel[amax_3000-None][alarm_control_panel.area1-state]
# name: test_alarm_control_panel[None-amax_3000][alarm_control_panel.area1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'changed_by': None,
@@ -51,7 +51,7 @@
'state': 'disarmed',
})
# ---
# name: test_alarm_control_panel[b5512-None][alarm_control_panel.area1-entry]
# name: test_alarm_control_panel[None-b5512][alarm_control_panel.area1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -86,7 +86,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_alarm_control_panel[b5512-None][alarm_control_panel.area1-state]
# name: test_alarm_control_panel[None-b5512][alarm_control_panel.area1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'changed_by': None,
@@ -103,7 +103,7 @@
'state': 'disarmed',
})
# ---
# name: test_alarm_control_panel[solution_3000-None][alarm_control_panel.area1-entry]
# name: test_alarm_control_panel[None-solution_3000][alarm_control_panel.area1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -138,7 +138,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_alarm_control_panel[solution_3000-None][alarm_control_panel.area1-state]
# name: test_alarm_control_panel[None-solution_3000][alarm_control_panel.area1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'changed_by': None,

View File

@@ -1,5 +1,5 @@
# serializer version: 1
# name: test_sensor[amax_3000-None][sensor.area1_burglary_alarm_issues-entry]
# name: test_sensor[None-amax_3000][sensor.area1_burglary_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -34,7 +34,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[amax_3000-None][sensor.area1_burglary_alarm_issues-state]
# name: test_sensor[None-amax_3000][sensor.area1_burglary_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Burglary alarm issues',
@@ -47,7 +47,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[amax_3000-None][sensor.area1_faulting_points-entry]
# name: test_sensor[None-amax_3000][sensor.area1_faulting_points-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -82,7 +82,7 @@
'unit_of_measurement': 'points',
})
# ---
# name: test_sensor[amax_3000-None][sensor.area1_faulting_points-state]
# name: test_sensor[None-amax_3000][sensor.area1_faulting_points-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Faulting points',
@@ -96,7 +96,7 @@
'state': '0',
})
# ---
# name: test_sensor[amax_3000-None][sensor.area1_fire_alarm_issues-entry]
# name: test_sensor[None-amax_3000][sensor.area1_fire_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -131,7 +131,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[amax_3000-None][sensor.area1_fire_alarm_issues-state]
# name: test_sensor[None-amax_3000][sensor.area1_fire_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Fire alarm issues',
@@ -144,7 +144,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[amax_3000-None][sensor.area1_gas_alarm_issues-entry]
# name: test_sensor[None-amax_3000][sensor.area1_gas_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -179,7 +179,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[amax_3000-None][sensor.area1_gas_alarm_issues-state]
# name: test_sensor[None-amax_3000][sensor.area1_gas_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Gas alarm issues',
@@ -192,7 +192,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[b5512-None][sensor.area1_burglary_alarm_issues-entry]
# name: test_sensor[None-b5512][sensor.area1_burglary_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -227,7 +227,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[b5512-None][sensor.area1_burglary_alarm_issues-state]
# name: test_sensor[None-b5512][sensor.area1_burglary_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Burglary alarm issues',
@@ -240,7 +240,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[b5512-None][sensor.area1_faulting_points-entry]
# name: test_sensor[None-b5512][sensor.area1_faulting_points-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -275,7 +275,7 @@
'unit_of_measurement': 'points',
})
# ---
# name: test_sensor[b5512-None][sensor.area1_faulting_points-state]
# name: test_sensor[None-b5512][sensor.area1_faulting_points-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Faulting points',
@@ -289,7 +289,7 @@
'state': '0',
})
# ---
# name: test_sensor[b5512-None][sensor.area1_fire_alarm_issues-entry]
# name: test_sensor[None-b5512][sensor.area1_fire_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -324,7 +324,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[b5512-None][sensor.area1_fire_alarm_issues-state]
# name: test_sensor[None-b5512][sensor.area1_fire_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Fire alarm issues',
@@ -337,7 +337,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[b5512-None][sensor.area1_gas_alarm_issues-entry]
# name: test_sensor[None-b5512][sensor.area1_gas_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -372,7 +372,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[b5512-None][sensor.area1_gas_alarm_issues-state]
# name: test_sensor[None-b5512][sensor.area1_gas_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Gas alarm issues',
@@ -385,7 +385,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_burglary_alarm_issues-entry]
# name: test_sensor[None-solution_3000][sensor.area1_burglary_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -420,7 +420,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_burglary_alarm_issues-state]
# name: test_sensor[None-solution_3000][sensor.area1_burglary_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Burglary alarm issues',
@@ -433,7 +433,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_faulting_points-entry]
# name: test_sensor[None-solution_3000][sensor.area1_faulting_points-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -468,7 +468,7 @@
'unit_of_measurement': 'points',
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_faulting_points-state]
# name: test_sensor[None-solution_3000][sensor.area1_faulting_points-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Faulting points',
@@ -482,7 +482,7 @@
'state': '0',
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_fire_alarm_issues-entry]
# name: test_sensor[None-solution_3000][sensor.area1_fire_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -517,7 +517,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_fire_alarm_issues-state]
# name: test_sensor[None-solution_3000][sensor.area1_fire_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Fire alarm issues',
@@ -530,7 +530,7 @@
'state': 'no_issues',
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_gas_alarm_issues-entry]
# name: test_sensor[None-solution_3000][sensor.area1_gas_alarm_issues-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -565,7 +565,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_sensor[solution_3000-None][sensor.area1_gas_alarm_issues-state]
# name: test_sensor[None-solution_3000][sensor.area1_gas_alarm_issues-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Area1 Gas alarm issues',

View File

@@ -1,5 +1,5 @@
# serializer version: 1
# name: test_switch[amax_3000-None][switch.main_door_locked-entry]
# name: test_switch[None-amax_3000][switch.main_door_locked-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -34,7 +34,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[amax_3000-None][switch.main_door_locked-state]
# name: test_switch[None-amax_3000][switch.main_door_locked-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Locked',
@@ -47,7 +47,7 @@
'state': 'on',
})
# ---
# name: test_switch[amax_3000-None][switch.main_door_momentarily_unlocked-entry]
# name: test_switch[None-amax_3000][switch.main_door_momentarily_unlocked-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -82,7 +82,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[amax_3000-None][switch.main_door_momentarily_unlocked-state]
# name: test_switch[None-amax_3000][switch.main_door_momentarily_unlocked-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Momentarily unlocked',
@@ -95,7 +95,7 @@
'state': 'off',
})
# ---
# name: test_switch[amax_3000-None][switch.main_door_secured-entry]
# name: test_switch[None-amax_3000][switch.main_door_secured-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -130,7 +130,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[amax_3000-None][switch.main_door_secured-state]
# name: test_switch[None-amax_3000][switch.main_door_secured-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Secured',
@@ -143,7 +143,7 @@
'state': 'off',
})
# ---
# name: test_switch[amax_3000-None][switch.output_a-entry]
# name: test_switch[None-amax_3000][switch.output_a-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -178,7 +178,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[amax_3000-None][switch.output_a-state]
# name: test_switch[None-amax_3000][switch.output_a-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Output A',
@@ -191,7 +191,7 @@
'state': 'off',
})
# ---
# name: test_switch[b5512-None][switch.main_door_locked-entry]
# name: test_switch[None-b5512][switch.main_door_locked-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -226,7 +226,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[b5512-None][switch.main_door_locked-state]
# name: test_switch[None-b5512][switch.main_door_locked-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Locked',
@@ -239,7 +239,7 @@
'state': 'on',
})
# ---
# name: test_switch[b5512-None][switch.main_door_momentarily_unlocked-entry]
# name: test_switch[None-b5512][switch.main_door_momentarily_unlocked-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -274,7 +274,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[b5512-None][switch.main_door_momentarily_unlocked-state]
# name: test_switch[None-b5512][switch.main_door_momentarily_unlocked-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Momentarily unlocked',
@@ -287,7 +287,7 @@
'state': 'off',
})
# ---
# name: test_switch[b5512-None][switch.main_door_secured-entry]
# name: test_switch[None-b5512][switch.main_door_secured-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -322,7 +322,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[b5512-None][switch.main_door_secured-state]
# name: test_switch[None-b5512][switch.main_door_secured-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Secured',
@@ -335,7 +335,7 @@
'state': 'off',
})
# ---
# name: test_switch[b5512-None][switch.output_a-entry]
# name: test_switch[None-b5512][switch.output_a-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -370,7 +370,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[b5512-None][switch.output_a-state]
# name: test_switch[None-b5512][switch.output_a-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Output A',
@@ -383,7 +383,7 @@
'state': 'off',
})
# ---
# name: test_switch[solution_3000-None][switch.main_door_locked-entry]
# name: test_switch[None-solution_3000][switch.main_door_locked-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -418,7 +418,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[solution_3000-None][switch.main_door_locked-state]
# name: test_switch[None-solution_3000][switch.main_door_locked-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Locked',
@@ -431,7 +431,7 @@
'state': 'on',
})
# ---
# name: test_switch[solution_3000-None][switch.main_door_momentarily_unlocked-entry]
# name: test_switch[None-solution_3000][switch.main_door_momentarily_unlocked-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -466,7 +466,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[solution_3000-None][switch.main_door_momentarily_unlocked-state]
# name: test_switch[None-solution_3000][switch.main_door_momentarily_unlocked-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Momentarily unlocked',
@@ -479,7 +479,7 @@
'state': 'off',
})
# ---
# name: test_switch[solution_3000-None][switch.main_door_secured-entry]
# name: test_switch[None-solution_3000][switch.main_door_secured-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -514,7 +514,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[solution_3000-None][switch.main_door_secured-state]
# name: test_switch[None-solution_3000][switch.main_door_secured-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Main Door Secured',
@@ -527,7 +527,7 @@
'state': 'off',
})
# ---
# name: test_switch[solution_3000-None][switch.output_a-entry]
# name: test_switch[None-solution_3000][switch.output_a-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -562,7 +562,7 @@
'unit_of_measurement': None,
})
# ---
# name: test_switch[solution_3000-None][switch.output_a-state]
# name: test_switch[None-solution_3000][switch.output_a-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Output A',

View File

@@ -3,6 +3,7 @@
from pathlib import Path
from unittest.mock import AsyncMock, patch
from freezegun import freeze_time
import httpx
from openai import PermissionDeniedError
import pytest
@@ -211,7 +212,7 @@ async def test_generate_data_with_attachments(
@pytest.mark.usefixtures("mock_init_component")
@pytest.mark.freeze_time("2025-06-14 22:59:00")
@freeze_time("2025-06-14 22:59:00")
@pytest.mark.parametrize("image_model", ["gpt-image-1", "gpt-image-1-mini"])
async def test_generate_image(
hass: HomeAssistant,

View File

@@ -2,6 +2,7 @@
import json
from freezegun import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -13,7 +14,7 @@ from tests.common import async_fire_mqtt_message
from tests.typing import MqttMockHAClient
@pytest.mark.freeze_time("2024-02-26 01:21:34")
@freeze_time("2024-02-26 01:21:34")
@pytest.mark.parametrize(
"sensor_suffix",
[

View File

@@ -1 +1,55 @@
"""Tests for the Plaato integration."""
from unittest.mock import patch
from freezegun import freeze_time
from pyplaato.models.airlock import PlaatoAirlock
from pyplaato.models.device import PlaatoDeviceType
from pyplaato.models.keg import PlaatoKeg
from homeassistant.components.plaato.const import (
CONF_DEVICE_NAME,
CONF_DEVICE_TYPE,
CONF_USE_WEBHOOK,
DOMAIN,
)
from homeassistant.const import CONF_TOKEN
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
# Note: It would be good to replace this test data
# with actual data from the API
AIRLOCK_DATA = {}
KEG_DATA = {}
@freeze_time("2024-05-24 12:00:00", tz_offset=0)
async def init_integration(
hass: HomeAssistant, device_type: PlaatoDeviceType
) -> MockConfigEntry:
"""Mock integration setup."""
with (
patch(
"homeassistant.components.plaato.coordinator.Plaato.get_airlock_data",
return_value=PlaatoAirlock(AIRLOCK_DATA),
),
patch(
"homeassistant.components.plaato.coordinator.Plaato.get_keg_data",
return_value=PlaatoKeg(KEG_DATA),
),
):
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USE_WEBHOOK: False,
CONF_TOKEN: "valid_token",
CONF_DEVICE_TYPE: device_type,
CONF_DEVICE_NAME: "device_name",
},
entry_id="123456",
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry

View File

@@ -1,62 +0,0 @@
"""Test fixtures for the Plaato integration."""
from collections.abc import AsyncGenerator
from unittest.mock import patch
from pyplaato.models.airlock import PlaatoAirlock
from pyplaato.models.device import PlaatoDeviceType
from pyplaato.models.keg import PlaatoKeg
import pytest
from homeassistant.components.plaato.const import (
CONF_DEVICE_NAME,
CONF_DEVICE_TYPE,
CONF_USE_WEBHOOK,
DOMAIN,
)
from homeassistant.const import CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
# Note: It would be good to replace this test data
# with actual data from the API
AIRLOCK_DATA = {}
KEG_DATA = {}
@pytest.fixture
async def init_integration(
hass: HomeAssistant,
device_type: PlaatoDeviceType,
platform: Platform,
) -> AsyncGenerator[MockConfigEntry]:
"""Mock integration setup."""
with (
patch(
"homeassistant.components.plaato.PLATFORMS",
[platform],
),
patch(
"homeassistant.components.plaato.coordinator.Plaato.get_airlock_data",
return_value=PlaatoAirlock(AIRLOCK_DATA),
),
patch(
"homeassistant.components.plaato.coordinator.Plaato.get_keg_data",
return_value=PlaatoKeg(KEG_DATA),
),
):
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_USE_WEBHOOK: False,
CONF_TOKEN: "valid_token",
CONF_DEVICE_TYPE: device_type,
CONF_DEVICE_NAME: "device_name",
},
entry_id="123456",
)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
yield entry

View File

@@ -1,5 +1,7 @@
"""Tests for the plaato binary sensors."""
from unittest.mock import patch
from pyplaato.models.device import PlaatoDeviceType
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -8,23 +10,24 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
from . import init_integration
@pytest.fixture
def platform() -> Platform:
"""Fixture to specify platform."""
return Platform.BINARY_SENSOR
from tests.common import snapshot_platform
# note: PlaatoDeviceType.Airlock does not provide binary sensors
@pytest.mark.parametrize("device_type", [PlaatoDeviceType.Keg])
@pytest.mark.freeze_time("2024-05-24 12:00:00", tz_offset=0)
async def test_binary_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
device_type: PlaatoDeviceType,
) -> None:
"""Test binary sensors."""
await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id)
with patch(
"homeassistant.components.plaato.PLATFORMS",
[Platform.BINARY_SENSOR],
):
entry = await init_integration(hass, device_type)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)

View File

@@ -1,5 +1,7 @@
"""Tests for the plaato sensors."""
from unittest.mock import patch
from pyplaato.models.device import PlaatoDeviceType
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -8,24 +10,25 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
from . import init_integration
@pytest.fixture
def platform() -> Platform:
"""Fixture to specify platform."""
return Platform.SENSOR
from tests.common import snapshot_platform
@pytest.mark.parametrize(
"device_type", [PlaatoDeviceType.Airlock, PlaatoDeviceType.Keg]
)
@pytest.mark.freeze_time("2024-05-24 12:00:00", tz_offset=0)
async def test_sensors(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
init_integration: MockConfigEntry,
snapshot: SnapshotAssertion,
device_type: PlaatoDeviceType,
) -> None:
"""Test sensors."""
await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id)
with patch(
"homeassistant.components.plaato.PLATFORMS",
[Platform.SENSOR],
):
entry = await init_integration(hass, device_type)
await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id)

View File

@@ -3,6 +3,7 @@
from collections.abc import AsyncGenerator
from unittest.mock import MagicMock, patch
from freezegun.api import freeze_time
from psnawp_api.core.psnawp_exceptions import (
PSNAWPClientError,
PSNAWPForbiddenError,
@@ -62,7 +63,7 @@ async def test_notify_platform(
"notify.testuser_direct_message_publicuniversalfriend",
],
)
@pytest.mark.freeze_time("2025-07-28T00:00:00+00:00")
@freeze_time("2025-07-28T00:00:00+00:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_send_message(
hass: HomeAssistant,

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from datetime import timedelta
from unittest.mock import MagicMock
from freezegun import freeze_time
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -26,7 +27,7 @@ from homeassistant.util import dt as dt_util
from tests.common import async_fire_time_changed, snapshot_platform
@pytest.mark.freeze_time("2022-03-12T15:24:26+00:00")
@freeze_time("2022-03-12T15:24:26+00:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
"load_platforms",

View File

@@ -9,6 +9,7 @@ import math
from typing import Any
from unittest.mock import patch
from freezegun.api import freeze_time
import pytest
from homeassistant.components import sensor
@@ -476,7 +477,7 @@ async def test_restore_sensor_save_state(
assert type(extra_data["native_value"]) is native_value_type
@pytest.mark.freeze_time("2020-02-08 15:00:00")
@freeze_time("2020-02-08 15:00:00")
async def test_restore_sensor_save_state_frozen_time_datetime(
hass: HomeAssistant,
hass_storage: dict[str, Any],
@@ -504,7 +505,7 @@ async def test_restore_sensor_save_state_frozen_time_datetime(
assert type(extra_data["native_value"]) is dict
@pytest.mark.freeze_time("2020-02-08 15:00:00")
@freeze_time("2020-02-08 15:00:00")
async def test_restore_sensor_save_state_frozen_time_date(
hass: HomeAssistant,
hass_storage: dict[str, Any],

View File

@@ -4,6 +4,7 @@ from collections.abc import Generator
from http import HTTPStatus
from unittest.mock import patch
from freezegun.api import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
@@ -27,7 +28,7 @@ def event_only() -> Generator[None]:
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.freeze_time("2025-01-01T03:30:00.000Z")
@freeze_time("2025-01-01T03:30:00.000Z")
async def test_setup(
hass: HomeAssistant,
config_entry: MockConfigEntry,
@@ -128,7 +129,7 @@ async def test_setup(
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.freeze_time("2025-01-01T03:30:00.000+00:00")
@freeze_time("2025-01-01T03:30:00.000+00:00")
async def test_webhook_event(
hass: HomeAssistant,
config_entry: MockConfigEntry,

View File

@@ -3,6 +3,7 @@
from datetime import datetime, timedelta
from unittest.mock import MagicMock
from freezegun import freeze_time
from freezegun.api import FrozenDateTimeFactory
from pysmhi import (
SMHIFirePointForecast,
@@ -65,7 +66,7 @@ async def test_setup_hass(
"to_load",
[1],
)
@pytest.mark.freeze_time(datetime(2023, 8, 7, 1, tzinfo=dt_util.UTC))
@freeze_time(datetime(2023, 8, 7, 1, tzinfo=dt_util.UTC))
async def test_clear_night(
hass: HomeAssistant,
mock_client: SMHIPointForecast,

View File

@@ -2,7 +2,7 @@
from unittest.mock import AsyncMock
import pytest
from freezegun import freeze_time
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
@@ -11,7 +11,7 @@ from . import async_init_integration, find_update_callback
from .const import MOCK_SNOO_DATA
@pytest.mark.freeze_time("2025-01-01 12:00:00")
@freeze_time("2025-01-01 12:00:00")
async def test_events(hass: HomeAssistant, bypass_api: AsyncMock) -> None:
"""Test events and check test values are correctly set."""
await async_init_integration(hass)
@@ -26,7 +26,7 @@ async def test_events(hass: HomeAssistant, bypass_api: AsyncMock) -> None:
)
@pytest.mark.freeze_time("2025-01-01 12:00:00")
@freeze_time("2025-01-01 12:00:00")
async def test_events_data_on_startup(
hass: HomeAssistant, bypass_api: AsyncMock
) -> None:

View File

@@ -1200,51 +1200,3 @@ CLIMATE_PANEL_SERVICE_INFO = BluetoothServiceInfoBleak(
connectable=True,
tx_power=-127,
)
SMART_THERMOSTAT_RADIATOR_SERVICE_INFO = BluetoothServiceInfoBleak(
name="Smart Thermostat Radiator",
manufacturer_data={2409: b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x116@",
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="Smart Thermostat Radiator",
manufacturer_data={2409: b"\xb0\xe9\xfe\xa2T|6\xe4\x00\x9c\xa3A\x00"},
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00 d\x00\x116@"},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Smart Thermostat Radiator"),
time=0,
connectable=True,
tx_power=-127,
)
S20_VACUUM_SERVICE_INFO = BluetoothServiceInfoBleak(
name="S20 Vacuum",
manufacturer_data={2409: b"\xb0\xe9\xfe\xc3\x1a!:\x01\x11\x1e\x00\x00d\x03"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xe0P",
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
address="AA:BB:CC:DD:EE:FF",
rssi=-60,
source="local",
advertisement=generate_advertisement_data(
local_name="S20 Vacuum",
manufacturer_data={2409: b"\xb0\xe9\xfe\xc3\x1a!:\x01\x11\x1e\x00\x00d\x03"},
service_data={
"0000fd3d-0000-1000-8000-00805f9b34fb": b"\x00\x00d\x00\x10\xe0P",
},
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
),
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "S20 Vacuum"),
time=0,
connectable=True,
tx_power=-127,
)

View File

@@ -1,118 +0,0 @@
"""Tests for the Switchbot climate integration."""
from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
from switchbot import SwitchbotOperationError
from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
HVACMode,
)
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from . import SMART_THERMOSTAT_RADIATOR_SERVICE_INFO
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info
@pytest.mark.parametrize(
("service", "service_data", "mock_method"),
[
(SERVICE_SET_HVAC_MODE, {"hvac_mode": HVACMode.HEAT}, "set_hvac_mode"),
(SERVICE_SET_PRESET_MODE, {"preset_mode": "manual"}, "set_preset_mode"),
(SERVICE_SET_TEMPERATURE, {"temperature": 22}, "set_target_temperature"),
],
)
async def test_smart_thermostat_radiator_controlling(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
service: str,
service_data: dict,
mock_method: str,
) -> None:
"""Test controlling the smart thermostat radiator with different services."""
inject_bluetooth_service_info(hass, SMART_THERMOSTAT_RADIATOR_SERVICE_INFO)
entry = mock_entry_encrypted_factory("smart_thermostat_radiator")
entity_id = "climate.test_name"
entry.add_to_hass(hass)
mocked_instance = AsyncMock(return_value=True)
mocked_none_instance = AsyncMock(return_value=None)
with patch.multiple(
"homeassistant.components.switchbot.climate.switchbot.SwitchbotSmartThermostatRadiator",
get_basic_info=mocked_none_instance,
update=mocked_none_instance,
**{mock_method: mocked_instance},
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
CLIMATE_DOMAIN,
service,
{**service_data, ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mocked_instance.assert_awaited_once()
@pytest.mark.parametrize(
("service", "service_data", "mock_method"),
[
(SERVICE_SET_HVAC_MODE, {"hvac_mode": HVACMode.HEAT}, "set_hvac_mode"),
(SERVICE_SET_PRESET_MODE, {"preset_mode": "manual"}, "set_preset_mode"),
(SERVICE_SET_TEMPERATURE, {"temperature": 22}, "set_target_temperature"),
],
)
@pytest.mark.parametrize(
("exception", "error_message"),
[
(
SwitchbotOperationError("Operation failed"),
"An error occurred while performing the action: Operation failed",
),
],
)
async def test_exception_handling_smart_thermostat_radiator_service(
hass: HomeAssistant,
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
service: str,
service_data: dict,
mock_method: str,
exception: Exception,
error_message: str,
) -> None:
"""Test exception handling for smart thermostat radiator service with exception."""
inject_bluetooth_service_info(hass, SMART_THERMOSTAT_RADIATOR_SERVICE_INFO)
entry = mock_entry_encrypted_factory("smart_thermostat_radiator")
entry.add_to_hass(hass)
entity_id = "climate.test_name"
mocked_none_instance = AsyncMock(return_value=None)
with patch.multiple(
"homeassistant.components.switchbot.climate.switchbot.SwitchbotSmartThermostatRadiator",
get_basic_info=mocked_none_instance,
update=mocked_none_instance,
**{mock_method: AsyncMock(side_effect=exception)},
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
with pytest.raises(HomeAssistantError, match=error_message):
await hass.services.async_call(
CLIMATE_DOMAIN,
service,
{**service_data, ATTR_ENTITY_ID: entity_id},
blocking=True,
)

View File

@@ -21,7 +21,6 @@ from . import (
K11_PLUS_VACUUM_SERVICE_INFO,
K20_VACUUM_SERVICE_INFO,
S10_VACUUM_SERVICE_INFO,
S20_VACUUM_SERVICE_INFO,
)
from tests.common import MockConfigEntry
@@ -37,7 +36,6 @@ from tests.components.bluetooth import inject_bluetooth_service_info
("k10_vacuum", K10_VACUUM_SERVICE_INFO),
("k10_pro_vacuum", K10_PRO_VACUUM_SERVICE_INFO),
("k11+_vacuum", K11_PLUS_VACUUM_SERVICE_INFO),
("s20_vacuum", S20_VACUUM_SERVICE_INFO),
],
)
@pytest.mark.parametrize(

View File

@@ -3,7 +3,7 @@
from datetime import datetime
from unittest.mock import AsyncMock, patch
import pytest
from freezegun.api import freeze_time
from telegram import Chat, Message
from telegram.constants import ChatType, ParseMode
@@ -19,7 +19,7 @@ from homeassistant.core import Context, HomeAssistant
from tests.common import async_capture_events
@pytest.mark.freeze_time("2025-01-09T12:00:00+00:00")
@freeze_time("2025-01-09T12:00:00+00:00")
async def test_send_message(
hass: HomeAssistant,
webhook_platform: None,

View File

@@ -244,7 +244,7 @@ async def test_v4_weather_legacy_entities(hass: HomeAssistant) -> None:
("service"),
[SERVICE_GET_FORECASTS],
)
@pytest.mark.freeze_time(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC))
@freeze_time(datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC))
async def test_v4_forecast_service(
hass: HomeAssistant,
snapshot: SnapshotAssertion,

View File

@@ -1,22 +1,58 @@
"""Test Utility Meter diagnostics."""
from aiohttp.test_utils import TestClient
from freezegun import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
from syrupy.filters import props
from homeassistant.auth.models import Credentials
from homeassistant.components.utility_meter.const import DOMAIN
from homeassistant.components.utility_meter.sensor import ATTR_LAST_RESET
from homeassistant.core import HomeAssistant, State
from tests.common import MockConfigEntry, mock_restore_cache_with_extra_data
from tests.common import (
CLIENT_ID,
MockConfigEntry,
MockUser,
mock_restore_cache_with_extra_data,
)
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
@pytest.mark.freeze_time("2024-04-06 00:00:00+00:00")
async def generate_new_hass_access_token(
hass: HomeAssistant, hass_admin_user: MockUser, hass_admin_credential: Credentials
) -> str:
"""Return an access token to access Home Assistant."""
await hass.auth.async_link_user(hass_admin_user, hass_admin_credential)
refresh_token = await hass.auth.async_create_refresh_token(
hass_admin_user, CLIENT_ID, credential=hass_admin_credential
)
return hass.auth.async_create_access_token(refresh_token)
def _get_test_client_generator(
hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, new_token: str
):
"""Return a test client generator.""."""
async def auth_client() -> TestClient:
return await aiohttp_client(
hass.http.app, headers={"Authorization": f"Bearer {new_token}"}
)
return auth_client
@freeze_time("2024-04-06 00:00:00+00:00")
@pytest.mark.usefixtures("socket_enabled")
async def test_diagnostics(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
aiohttp_client: ClientSessionGenerator,
hass_admin_user: MockUser,
hass_admin_credential: Credentials,
snapshot: SnapshotAssertion,
) -> None:
"""Test generating diagnostics for a config entry."""
@@ -94,6 +130,15 @@ async def test_diagnostics(
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry)
# Since we are freezing time only when we enter this test, we need to
# manually create a new token and clients since the token created by
# the fixtures would not be valid.
new_token = await generate_new_hass_access_token(
hass, hass_admin_user, hass_admin_credential
)
diag = await get_diagnostics_for_config_entry(
hass, _get_test_client_generator(hass, aiohttp_client, new_token), config_entry
)
assert diag == snapshot(exclude=props("entry_id", "created_at", "modified_at"))

View File

@@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.velux import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceRegistry
@@ -96,11 +95,3 @@ async def test_rain_sensor_device_association(
# Verify device has correct identifiers
assert ("velux", mock_window.serial_number) in device_entry.identifiers
assert device_entry.name == mock_window.name
# Verify via_device is gateway
assert device_entry.via_device_id is not None
via_device_entry = device_registry.async_get(device_entry.via_device_id)
assert via_device_entry is not None
assert via_device_entry.identifiers == {
(DOMAIN, f"gateway_{mock_config_entry.entry_id}")
}

View File

@@ -6,6 +6,7 @@ from functools import partial
from pathlib import Path
from unittest.mock import patch
from freezegun import freeze_time
import pytest
from homeassistant.components import wake_word
@@ -169,7 +170,7 @@ async def test_config_entry_unload(
assert config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.freeze_time("2023-06-22 10:30:00+00:00")
@freeze_time("2023-06-22 10:30:00+00:00")
@pytest.mark.parametrize(
("wake_word_id", "expected_ww", "expected_phrase"),
[

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from unittest.mock import Mock
from freezegun import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
from yalesmartalarmclient.exceptions import UnknownError
@@ -17,7 +18,7 @@ from homeassistant.helpers import entity_registry as er
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.freeze_time("2024-04-29T18:00:00.612351+00:00")
@freeze_time("2024-04-29T18:00:00.612351+00:00")
@pytest.mark.parametrize(
"load_platforms",
[[Platform.BUTTON]],

View File

@@ -3,6 +3,7 @@
from collections.abc import Callable, Coroutine
from unittest.mock import patch
from freezegun import freeze_time
import pytest
from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from zigpy.device import Device
@@ -45,29 +46,23 @@ def button_platform_only():
@pytest.fixture
def speed_up_radio_mgr():
"""Speed up the radio manager connection time by removing delays.
async def setup_zha_integration(
hass: HomeAssistant, setup_zha: Callable[..., Coroutine[None]]
):
"""Set up ZHA component."""
This fixture replaces the fixture in conftest.py by patching the connect
and shutdown delays to 0 to allow waiting for the patched delays when
running tests with time frozen, which otherwise blocks forever.
"""
with (
patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0),
patch("zha.application.gateway.SHUT_DOWN_DELAY_S", 0),
):
yield
# if we call this in the test itself the test hangs forever
await setup_zha()
@pytest.mark.freeze_time("2021-11-04 17:37:00", tz_offset=-1)
@freeze_time("2021-11-04 17:37:00", tz_offset=-1)
async def test_button(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
setup_zha: Callable[..., Coroutine[None]],
setup_zha_integration, # pylint: disable=unused-argument
zigpy_device_mock: Callable[..., Device],
) -> None:
"""Test ZHA button platform."""
await setup_zha()
gateway = get_zha_gateway(hass)
gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass)

View File

@@ -8,6 +8,7 @@ from copy import deepcopy
from typing import TYPE_CHECKING
from unittest.mock import ANY, AsyncMock, MagicMock, call, patch
from freezegun import freeze_time
import pytest
import voluptuous as vol
from zha.application.const import (
@@ -93,21 +94,6 @@ def required_platform_only():
yield
@pytest.fixture
def speed_up_radio_mgr():
"""Speed up the radio manager connection time by removing delays.
This fixture replaces the fixture in conftest.py by patching the connect
and shutdown delays to 0 to allow waiting for the patched delays when
running tests with time frozen, which otherwise blocks forever.
"""
with (
patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0),
patch("zha.application.gateway.SHUT_DOWN_DELAY_S", 0),
):
yield
@pytest.fixture
async def zha_client(
hass: HomeAssistant,
@@ -231,7 +217,7 @@ async def test_device_cluster_commands(zha_client) -> None:
assert command[TYPE] is not None
@pytest.mark.freeze_time("2023-09-23 20:16:00+00:00")
@freeze_time("2023-09-23 20:16:00+00:00")
async def test_list_devices(zha_client) -> None:
"""Test getting ZHA devices."""
await zha_client.send_json({ID: 5, TYPE: "zha/devices"})