This commit is contained in:
J. Nick Koston 2025-04-15 10:36:08 -10:00
commit d8f6551dab
No known key found for this signature in database
40 changed files with 1027 additions and 92 deletions

View File

@ -1,5 +1,6 @@
name: Report an issue with Home Assistant Core
description: Report an issue with Home Assistant Core.
type: Bug
body:
- type: markdown
attributes:

View File

@ -1317,7 +1317,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.0
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
flags: full-suite
@ -1459,7 +1459,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.0
uses: codecov/codecov-action@v5.4.2
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -28,10 +28,10 @@
"name": "Thermostat",
"state": {
"off": "[%key:common::state::off%]",
"auto": "[%key:common::state::auto%]",
"heat": "Heat",
"cool": "Cool",
"heat_cool": "Heat/Cool",
"auto": "Auto",
"dry": "Dry",
"fan_only": "Fan only"
},
@ -50,7 +50,7 @@
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"auto": "Auto",
"auto": "[%key:common::state::auto%]",
"low": "[%key:common::state::low%]",
"medium": "[%key:common::state::medium%]",
"high": "[%key:common::state::high%]",
@ -69,13 +69,13 @@
"hvac_action": {
"name": "Current action",
"state": {
"off": "[%key:common::state::off%]",
"idle": "[%key:common::state::idle%]",
"cooling": "Cooling",
"defrosting": "Defrosting",
"drying": "Drying",
"fan": "Fan",
"heating": "Heating",
"idle": "[%key:common::state::idle%]",
"off": "[%key:common::state::off%]",
"preheating": "Preheating"
}
},
@ -258,7 +258,7 @@
"hvac_mode": {
"options": {
"off": "[%key:common::state::off%]",
"auto": "Auto",
"auto": "[%key:common::state::auto%]",
"cool": "Cool",
"dry": "Dry",
"fan_only": "Fan only",

View File

@ -75,4 +75,7 @@ class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
@property
def is_on(self) -> bool:
"""Return True if switch is on."""
return self.coordinator.data[OTHER][self._device.index].status == STATE_ON
return (
self.coordinator.data[self._device.type][self._device.index].status
== STATE_ON
)

View File

@ -9,7 +9,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.SENSOR]
PLATFORMS = [Platform.CLIMATE, Platform.LIGHT, Platform.NUMBER, Platform.SENSOR]
async def async_setup_entry(

View File

@ -1,5 +1,22 @@
{
"entity": {
"number": {
"manual_speed": {
"default": "mdi:pump"
},
"day_speed": {
"default": "mdi:weather-sunny"
},
"night_speed": {
"default": "mdi:moon-waning-crescent"
},
"temperature_offset": {
"default": "mdi:thermometer"
},
"night_temperature_offset": {
"default": "mdi:thermometer"
}
},
"sensor": {
"current_speed": {
"default": "mdi:pump"

View File

@ -0,0 +1,177 @@
"""EHEIM Digital numbers."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.heater import EheimDigitalHeater
from eheimdigital.types import HeaterUnit
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import (
PERCENTAGE,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
EntityCategory,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalNumberDescription(NumberEntityDescription, Generic[_DeviceT_co]):
"""Class describing EHEIM Digital sensor entities."""
value_fn: Callable[[_DeviceT_co], float | None]
set_value_fn: Callable[[_DeviceT_co, float], Awaitable[None]]
uom_fn: Callable[[_DeviceT_co], str] | None = None
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalNumberDescription[EheimDigitalClassicVario], ...
] = (
EheimDigitalNumberDescription[EheimDigitalClassicVario](
key="manual_speed",
translation_key="manual_speed",
entity_category=EntityCategory.CONFIG,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.manual_speed,
set_value_fn=lambda device, value: device.set_manual_speed(int(value)),
),
EheimDigitalNumberDescription[EheimDigitalClassicVario](
key="day_speed",
translation_key="day_speed",
entity_category=EntityCategory.CONFIG,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.day_speed,
set_value_fn=lambda device, value: device.set_day_speed(int(value)),
),
EheimDigitalNumberDescription[EheimDigitalClassicVario](
key="night_speed",
translation_key="night_speed",
entity_category=EntityCategory.CONFIG,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.night_speed,
set_value_fn=lambda device, value: device.set_night_speed(int(value)),
),
)
HEATER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalHeater], ...] = (
EheimDigitalNumberDescription[EheimDigitalHeater](
key="temperature_offset",
translation_key="temperature_offset",
entity_category=EntityCategory.CONFIG,
native_min_value=-3,
native_max_value=3,
native_step=PRECISION_TENTHS,
device_class=NumberDeviceClass.TEMPERATURE,
uom_fn=lambda device: (
UnitOfTemperature.CELSIUS
if device.temperature_unit is HeaterUnit.CELSIUS
else UnitOfTemperature.FAHRENHEIT
),
value_fn=lambda device: device.temperature_offset,
set_value_fn=lambda device, value: device.set_temperature_offset(value),
),
EheimDigitalNumberDescription[EheimDigitalHeater](
key="night_temperature_offset",
translation_key="night_temperature_offset",
entity_category=EntityCategory.CONFIG,
native_min_value=-5,
native_max_value=5,
native_step=PRECISION_HALVES,
device_class=NumberDeviceClass.TEMPERATURE,
uom_fn=lambda device: (
UnitOfTemperature.CELSIUS
if device.temperature_unit is HeaterUnit.CELSIUS
else UnitOfTemperature.FAHRENHEIT
),
value_fn=lambda device: device.night_temperature_offset,
set_value_fn=lambda device, value: device.set_night_temperature_offset(value),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: EheimDigitalConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the callbacks for the coordinator so numbers can be added as devices are found."""
coordinator = entry.runtime_data
def async_setup_device_entities(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the number entities for one or multiple devices."""
entities: list[EheimDigitalNumber[EheimDigitalDevice]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
EheimDigitalNumber[EheimDigitalClassicVario](
coordinator, device, description
)
for description in CLASSICVARIO_DESCRIPTIONS
)
if isinstance(device, EheimDigitalHeater):
entities.extend(
EheimDigitalNumber[EheimDigitalHeater](
coordinator, device, description
)
for description in HEATER_DESCRIPTIONS
)
async_add_entities(entities)
coordinator.add_platform_callback(async_setup_device_entities)
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalNumber(
EheimDigitalEntity[_DeviceT_co], NumberEntity, Generic[_DeviceT_co]
):
"""Represent a EHEIM Digital number entity."""
entity_description: EheimDigitalNumberDescription[_DeviceT_co]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalNumberDescription[_DeviceT_co],
) -> None:
"""Initialize an EHEIM Digital number entity."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{self._device_address}_{description.key}"
@override
async def async_set_native_value(self, value: float) -> None:
return await self.entity_description.set_value_fn(self._device, value)
@override
def _async_update_attrs(self) -> None:
self._attr_native_value = self.entity_description.value_fn(self._device)
self._attr_native_unit_of_measurement = (
self.entity_description.uom_fn(self._device)
if self.entity_description.uom_fn
else self.entity_description.native_unit_of_measurement
)

View File

@ -47,6 +47,23 @@
}
}
},
"number": {
"manual_speed": {
"name": "Manual speed"
},
"day_speed": {
"name": "Day speed"
},
"night_speed": {
"name": "Night speed"
},
"temperature_offset": {
"name": "Temperature offset"
},
"night_temperature_offset": {
"name": "Night temperature offset"
}
},
"sensor": {
"current_speed": {
"name": "Current speed"

View File

@ -47,6 +47,7 @@ from .const import (
DOMAIN,
)
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
from .manager import async_replace_device
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
@ -74,6 +75,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# The ESPHome name as per its config
self._device_name: str | None = None
self._device_mac: str | None = None
self._entry_with_name_conflict: ConfigEntry | None = None
async def _async_step_user_base(
self, user_input: dict[str, Any] | None = None, error: str | None = None
@ -137,7 +139,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle reauthorization flow when encryption was removed."""
if user_input is not None:
self._noise_psk = None
return self._async_get_entry()
return await self._async_get_entry_or_resolve_conflict()
return self.async_show_form(
step_id="reauth_encryption_removed_confirm",
@ -227,7 +229,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_authenticate()
self._password = ""
return self._async_get_entry()
return await self._async_get_entry_or_resolve_conflict()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
@ -354,6 +356,77 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
)
return self.async_abort(reason="service_received")
async def async_step_name_conflict(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle name conflict resolution."""
assert self._entry_with_name_conflict is not None
assert self._entry_with_name_conflict.unique_id is not None
assert self.unique_id is not None
assert self._device_name is not None
return self.async_show_menu(
step_id="name_conflict",
menu_options=["name_conflict_migrate", "name_conflict_overwrite"],
description_placeholders={
"existing_mac": format_mac(self._entry_with_name_conflict.unique_id),
"existing_title": self._entry_with_name_conflict.title,
"mac": format_mac(self.unique_id),
"name": self._device_name,
},
)
async def async_step_name_conflict_migrate(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle migration of existing entry."""
assert self._entry_with_name_conflict is not None
assert self._entry_with_name_conflict.unique_id is not None
assert self.unique_id is not None
assert self._device_name is not None
assert self._host is not None
old_mac = format_mac(self._entry_with_name_conflict.unique_id)
new_mac = format_mac(self.unique_id)
entry_id = self._entry_with_name_conflict.entry_id
self.hass.config_entries.async_update_entry(
self._entry_with_name_conflict,
data={
**self._entry_with_name_conflict.data,
CONF_HOST: self._host,
CONF_PORT: self._port or 6053,
CONF_PASSWORD: self._password or "",
CONF_NOISE_PSK: self._noise_psk or "",
},
)
await async_replace_device(self.hass, entry_id, old_mac, new_mac)
self.hass.config_entries.async_schedule_reload(entry_id)
return self.async_abort(
reason="name_conflict_migrated",
description_placeholders={
"existing_mac": old_mac,
"mac": new_mac,
"name": self._device_name,
},
)
async def async_step_name_conflict_overwrite(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle creating a new entry by removing the old one and creating new."""
assert self._entry_with_name_conflict is not None
await self.hass.config_entries.async_remove(
self._entry_with_name_conflict.entry_id
)
return self._async_get_entry()
async def _async_get_entry_or_resolve_conflict(self) -> ConfigFlowResult:
"""Return the entry or resolve a conflict."""
if self.source != SOURCE_REAUTH:
for entry in self._async_current_entries(include_ignore=False):
if entry.data.get(CONF_DEVICE_NAME) == self._device_name:
self._entry_with_name_conflict = entry
return await self.async_step_name_conflict()
return self._async_get_entry()
@callback
def _async_get_entry(self) -> ConfigFlowResult:
config_data = {
@ -407,7 +480,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
error = await self.try_login()
if error:
return await self.async_step_authenticate(error=error)
return self._async_get_entry()
return await self._async_get_entry_or_resolve_conflict()
errors = {}
if error is not None:

View File

@ -9,7 +9,8 @@
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
"mqtt_missing_api": "Missing API port in MQTT properties.",
"mqtt_missing_ip": "Missing IP address in MQTT properties.",
"mqtt_missing_payload": "Missing MQTT Payload."
"mqtt_missing_payload": "Missing MQTT Payload.",
"name_conflict_migrated": "The configuration for `{name}` has been migrated to a new device with MAC address `{mac}` from `{existing_mac}`."
},
"error": {
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",

View File

@ -12,7 +12,7 @@ from pyfibaro.fibaro_client import (
FibaroClient,
FibaroConnectFailed,
)
from pyfibaro.fibaro_data_helper import read_rooms
from pyfibaro.fibaro_data_helper import find_master_devices, read_rooms
from pyfibaro.fibaro_device import DeviceModel
from pyfibaro.fibaro_device_manager import FibaroDeviceManager
from pyfibaro.fibaro_info import InfoModel
@ -176,35 +176,18 @@ class FibaroController:
platform = Platform.LIGHT
return platform
def _create_device_info(
self, device: DeviceModel, devices: list[DeviceModel]
) -> None:
"""Create the device info. Unrooted entities are directly shown below the home center."""
def _create_device_info(self, main_device: DeviceModel) -> None:
"""Create the device info for a main device."""
# The home center is always id 1 (z-wave primary controller)
if device.parent_fibaro_id <= 1:
return
master_entity: DeviceModel | None = None
if device.parent_fibaro_id == 1:
master_entity = device
else:
for parent in devices:
if parent.fibaro_id == device.parent_fibaro_id:
master_entity = parent
if master_entity is None:
_LOGGER.error("Parent with id %s not found", device.parent_fibaro_id)
return
if "zwaveCompany" in master_entity.properties:
manufacturer = master_entity.properties.get("zwaveCompany")
if "zwaveCompany" in main_device.properties:
manufacturer = main_device.properties.get("zwaveCompany")
else:
manufacturer = None
self._device_infos[master_entity.fibaro_id] = DeviceInfo(
identifiers={(DOMAIN, master_entity.fibaro_id)},
self._device_infos[main_device.fibaro_id] = DeviceInfo(
identifiers={(DOMAIN, main_device.fibaro_id)},
manufacturer=manufacturer,
name=master_entity.name,
name=main_device.name,
via_device=(DOMAIN, self.hub_serial),
)
@ -239,6 +222,10 @@ class FibaroController:
def _read_devices(self) -> None:
"""Read and process the device list."""
devices = self._fibaro_device_manager.get_devices()
for main_device in find_master_devices(devices):
self._create_device_info(main_device)
self._device_map = {}
last_climate_parent = None
last_endpoint = None
@ -258,7 +245,6 @@ class FibaroController:
if platform is None:
continue
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
self._create_device_info(device, devices)
self._device_map[device.fibaro_id] = device
_LOGGER.debug(
"%s (%s, %s) -> %s %s",

View File

@ -105,7 +105,7 @@
"sensor": {
"heating": {
"state": {
"manual": "Manual",
"manual": "[%key:common::state::manual%]",
"off": "[%key:common::state::off%]",
"schedule": "Schedule"
}

View File

@ -37,6 +37,8 @@ class TimePattern:
if isinstance(value, str) and value.startswith("/"):
number = int(value[1:])
if number == 0:
raise vol.Invalid(f"must be a value between 1 and {self.maximum}")
else:
value = number = int(value)

View File

@ -115,7 +115,7 @@
"state": {
"right_handed": "Right-handed",
"left_handed": "Left-handed",
"auto": "Auto"
"auto": "[%key:common::state::auto%]"
}
},
"animation_speed": {

View File

@ -123,7 +123,7 @@
"mid": "[%key:common::state::medium%]",
"high": "[%key:common::state::high%]",
"power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]"
"auto": "[%key:common::state::auto%]"
}
},
"preset_mode": {
@ -343,7 +343,7 @@
"growth_mode": {
"name": "Mode",
"state": {
"standard": "Auto",
"standard": "[%key:common::state::auto%]",
"ext_leaf": "Vegetables",
"ext_herb": "Herbs",
"ext_flower": "Flowers",
@ -353,7 +353,7 @@
"growth_mode_for_location": {
"name": "{location} mode",
"state": {
"standard": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
"standard": "[%key:common::state::auto%]",
"ext_leaf": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_leaf%]",
"ext_herb": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_herb%]",
"ext_flower": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::ext_flower%]",
@ -581,7 +581,7 @@
"name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]",
"state": {
"off": "[%key:common::state::off%]",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
"auto": "[%key:common::state::auto%]",
"power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
"replace": "Replace filter",
"smart_power": "Smart safe storage",
@ -599,7 +599,7 @@
"name": "Operating mode",
"state": {
"air_clean": "Purify",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
"auto": "[%key:common::state::auto%]",
"clothes_dry": "Laundry",
"edge": "Edge cleaning",
"heat_pump": "Heat pump",
@ -649,7 +649,7 @@
"current_dish_washing_course": {
"name": "Current cycle",
"state": {
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
"auto": "[%key:common::state::auto%]",
"heavy": "Intensive",
"delicate": "Delicate",
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
@ -881,7 +881,7 @@
"high": "[%key:common::state::high%]",
"power": "Turbo",
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
"auto": "[%key:common::state::auto%]",
"wind_1": "Step 1",
"wind_2": "Step 2",
"wind_3": "Step 3",
@ -905,7 +905,7 @@
"name": "Operating mode",
"state": {
"air_clean": "Purifying",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
"auto": "[%key:common::state::auto%]",
"baby_care": "[%key:component::lg_thinq::entity::sensor::personalization_mode::state::baby%]",
"circulator": "Booster",
"clean": "Single",
@ -1016,7 +1016,7 @@
"name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]",
"state": {
"off": "[%key:common::state::off%]",
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
"auto": "[%key:common::state::auto%]",
"power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
"replace": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::replace%]",
"smart_power": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::smart_power%]",

View File

@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/lutron",
"iot_class": "local_polling",
"loggers": ["pylutron"],
"requirements": ["pylutron==0.2.16"],
"requirements": ["pylutron==0.2.18"],
"single_config_entry": true
}

View File

@ -1,7 +1,7 @@
{
"issues": {
"invalid_platform_config": {
"title": "Invalid config found for mqtt {domain} item",
"title": "Invalid config found for MQTT {domain} item",
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
},
"invalid_unit_of_measurement": {
@ -68,7 +68,7 @@
"title": "Starting add-on"
},
"hassio_confirm": {
"title": "MQTT Broker via Home Assistant add-on",
"title": "MQTT broker via Home Assistant add-on",
"description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?"
},
"reauth_confirm": {
@ -153,7 +153,7 @@
},
"sections": {
"mqtt_settings": {
"name": "MQTT Settings",
"name": "MQTT settings",
"data": {
"qos": "QoS"
},
@ -480,7 +480,7 @@
"set_ca_cert": {
"options": {
"off": "[%key:common::state::off%]",
"auto": "Auto",
"auto": "[%key:common::state::auto%]",
"custom": "Custom"
}
},

View File

@ -71,14 +71,14 @@
"state_attributes": {
"preset_mode": {
"state": {
"auto": "Auto",
"auto": "[%key:common::state::auto%]",
"manual": "[%key:common::state::manual%]",
"comfort-1": "Comfort 1",
"comfort-2": "Comfort 2",
"drying": "Drying",
"external": "External",
"freeze": "Freeze",
"frost_protection": "Frost protection",
"manual": "Manual",
"night": "Night",
"prog": "Prog"
}

View File

@ -23,7 +23,7 @@
},
"data_description": {
"password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
"host": "The hostname or IP-address of your Smile. You can find it in your router or the Plugwise App.",
"host": "The hostname or IP-address of your Smile. You can find it in your router or the Plugwise app.",
"port": "By default your Smile uses port 80, normally you should not have to change this.",
"username": "Default is `smile`, or `stretch` for the legacy Stretch."
}
@ -113,7 +113,7 @@
"name": "DHW mode",
"state": {
"off": "[%key:common::state::off%]",
"auto": "Auto",
"auto": "[%key:common::state::auto%]",
"boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]"
}

View File

@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.13.1"]
"requirements": ["reolink-aio==0.13.2"]
}

View File

@ -652,7 +652,7 @@
"name": "Floodlight mode",
"state": {
"off": "[%key:common::state::off%]",
"auto": "[%key:component::reolink::entity::select::day_night_mode::state::auto%]",
"auto": "[%key:common::state::auto%]",
"onatnight": "On at night",
"schedule": "Schedule",
"adaptive": "Adaptive",
@ -662,7 +662,7 @@
"day_night_mode": {
"name": "Day night mode",
"state": {
"auto": "Auto",
"auto": "[%key:common::state::auto%]",
"color": "Color",
"blackwhite": "Black & white"
}
@ -691,7 +691,7 @@
"name": "Doorbell LED",
"state": {
"stayoff": "Stay off",
"auto": "[%key:component::reolink::entity::select::day_night_mode::state::auto%]",
"auto": "[%key:common::state::auto%]",
"alwaysonatnight": "Auto & always on at night",
"always": "Always on",
"alwayson": "Always on"
@ -702,7 +702,7 @@
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"auto": "[%key:component::reolink::entity::select::day_night_mode::state::auto%]"
"auto": "[%key:common::state::auto%]"
}
},
"binning_mode": {
@ -710,7 +710,7 @@
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]",
"auto": "[%key:component::reolink::entity::select::day_night_mode::state::auto%]"
"auto": "[%key:common::state::auto%]"
}
},
"hub_alarm_ringtone": {

View File

@ -426,11 +426,11 @@
"state_attributes": {
"fan_speed": {
"state": {
"auto": "Auto",
"off": "[%key:common::state::off%]",
"auto": "[%key:common::state::auto%]",
"balanced": "Balanced",
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
"gentle": "Gentle",
"off": "[%key:common::state::off%]",
"max": "[%key:component::roborock::entity::select::mop_intensity::state::max%]",
"max_plus": "Max plus",
"medium": "[%key:common::state::medium%]",

View File

@ -53,7 +53,7 @@
"state_attributes": {
"preset_mode": {
"state": {
"auto": "Auto"
"auto": "[%key:common::state::auto%]"
}
}
}
@ -139,7 +139,7 @@
"description": "Adds a meter reading to Tado Energy IQ.",
"fields": {
"config_entry": {
"name": "Config Entry",
"name": "Config entry",
"description": "Config entry to add meter reading to."
},
"reading": {

View File

@ -288,9 +288,9 @@
"motion_sensitivity": {
"name": "Motion detection sensitivity",
"state": {
"0": "Low sensitivity",
"1": "Medium sensitivity",
"2": "High sensitivity"
"0": "[%key:common::state::low%]",
"1": "[%key:common::state::medium%]",
"2": "[%key:common::state::high%]"
}
},
"record_mode": {
@ -404,7 +404,7 @@
"humidifier_spray_mode": {
"name": "Spray mode",
"state": {
"auto": "Auto",
"auto": "[%key:common::state::auto%]",
"health": "Health",
"sleep": "Sleep",
"humidity": "Humidity",

View File

@ -43,4 +43,4 @@ class UptimeRobotBinarySensor(UptimeRobotEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return True if the entity is on."""
return self.monitor_available
return bool(self.monitor.status == 2)

View File

@ -59,8 +59,3 @@ class UptimeRobotEntity(CoordinatorEntity[UptimeRobotDataUpdateCoordinator]):
),
self._monitor,
)
@property
def monitor_available(self) -> bool:
"""Returtn if the monitor is available."""
return bool(self.monitor.status == 2)

View File

@ -0,0 +1,92 @@
rules:
# Bronze
action-setup:
status: exempt
comment: no actions
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage:
status: todo
comment: fix name and docstring
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: no actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: no 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: todo
comment: we should not swallow the exception in switch.py
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable:
status: todo
comment: Change the type of the coordinator data to be a dict[str, UptimeRobotMonitor] so we can just do a dict look up instead of iterating over the whole list
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage:
status: todo
comment: recheck typos
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: device not discoverable
discovery:
status: exempt
comment: device not discoverable
docs-data-update: done
docs-examples: done
docs-known-limitations:
status: exempt
comment: no known limitations, yet
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: todo
comment: create entities on runtime instead of triggering a reload
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: no known use case
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: todo
comment: handle API key change/update
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices:
status: todo
comment: We should remove the config entry from the device rather than remove the device
# Platinum
async-dependency: done
inject-websession: done
strict-typing:
status: todo
comment: Requirement 'pyuptimerobot==22.2.0' appears untyped

View File

@ -24,8 +24,6 @@ type WhirlpoolConfigEntry = ConfigEntry[AppliancesManager]
async def async_setup_entry(hass: HomeAssistant, entry: WhirlpoolConfigEntry) -> bool:
"""Set up Whirlpool Sixth Sense from a config entry."""
hass.data.setdefault(DOMAIN, {})
session = async_get_clientsession(hass)
region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")]
brand = CONF_BRANDS_MAP[entry.data.get(CONF_BRAND, "Whirlpool")]

View File

@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["zeroconf"],
"quality_scale": "internal",
"requirements": ["zeroconf==0.146.0"]
"requirements": ["zeroconf==0.146.5"]
}

View File

@ -75,7 +75,7 @@ voluptuous-serialize==2.6.0
voluptuous==0.15.2
webrtc-models==0.3.0
yarl==1.19.0
zeroconf==0.146.0
zeroconf==0.146.5
# Constrain pycryptodome to avoid vulnerability
# see https://github.com/home-assistant/core/pull/16238

View File

@ -123,7 +123,7 @@ dependencies = [
"voluptuous-openapi==0.0.6",
"yarl==1.19.0",
"webrtc-models==0.3.0",
"zeroconf==0.146.0",
"zeroconf==0.146.5",
]
[project.urls]

2
requirements.txt generated
View File

@ -60,4 +60,4 @@ voluptuous-serialize==2.6.0
voluptuous-openapi==0.0.6
yarl==1.19.0
webrtc-models==0.3.0
zeroconf==0.146.0
zeroconf==0.146.5

6
requirements_all.txt generated
View File

@ -2113,7 +2113,7 @@ pylitterbot==2024.0.0
pylutron-caseta==0.24.0
# homeassistant.components.lutron
pylutron==0.2.16
pylutron==0.2.18
# homeassistant.components.mailgun
pymailgunner==1.4
@ -2633,7 +2633,7 @@ renault-api==0.2.9
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.13.1
reolink-aio==0.13.2
# homeassistant.components.idteck_prox
rfk101py==0.0.1
@ -3152,7 +3152,7 @@ zabbix-utils==2.0.2
zamg==0.3.6
# homeassistant.components.zeroconf
zeroconf==0.146.0
zeroconf==0.146.5
# homeassistant.components.zeversolar
zeversolar==0.3.2

View File

@ -1728,7 +1728,7 @@ pylitterbot==2024.0.0
pylutron-caseta==0.24.0
# homeassistant.components.lutron
pylutron==0.2.16
pylutron==0.2.18
# homeassistant.components.mailgun
pymailgunner==1.4
@ -2137,7 +2137,7 @@ renault-api==0.2.9
renson-endura-delta==1.7.2
# homeassistant.components.reolink
reolink-aio==0.13.1
reolink-aio==0.13.2
# homeassistant.components.rflink
rflink==0.0.66
@ -2548,7 +2548,7 @@ yt-dlp[default]==2025.03.26
zamg==0.3.6
# homeassistant.components.zeroconf
zeroconf==0.146.0
zeroconf==0.146.5
# homeassistant.components.zeversolar
zeversolar==0.3.2

View File

@ -1059,7 +1059,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"upcloud",
"upnp",
"uptime",
"uptimerobot",
"usb",
"usgs_earthquakes_feed",
"utility_meter",

View File

@ -61,6 +61,8 @@ def heater_mock():
heater_mock.temperature_unit = HeaterUnit.CELSIUS
heater_mock.current_temperature = 24.2
heater_mock.target_temperature = 25.5
heater_mock.temperature_offset = 0.1
heater_mock.night_temperature_offset = -0.2
heater_mock.is_heating = True
heater_mock.is_active = True
heater_mock.operation_mode = HeaterMode.MANUAL
@ -77,6 +79,9 @@ def classic_vario_mock():
classic_vario_mock.aquarium_name = "Mock Aquarium"
classic_vario_mock.sw_version = "1.0.0_1.0.0"
classic_vario_mock.current_speed = 75
classic_vario_mock.manual_speed = 75
classic_vario_mock.day_speed = 80
classic_vario_mock.night_speed = 20
classic_vario_mock.is_active = True
classic_vario_mock.filter_mode = FilterMode.MANUAL
classic_vario_mock.error_code = FilterErrorCode.NO_ERROR

View File

@ -0,0 +1,286 @@
# serializer version: 1
# name: test_setup[number.mock_classicvario_day_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100.0,
'min': 0.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.mock_classicvario_day_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Day speed',
'platform': 'eheimdigital',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'day_speed',
'unique_id': '00:00:00:00:00:03_day_speed',
'unit_of_measurement': '%',
})
# ---
# name: test_setup[number.mock_classicvario_day_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock classicVARIO Day speed',
'max': 100.0,
'min': 0.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.mock_classicvario_day_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[number.mock_classicvario_manual_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100.0,
'min': 0.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.mock_classicvario_manual_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Manual speed',
'platform': 'eheimdigital',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'manual_speed',
'unique_id': '00:00:00:00:00:03_manual_speed',
'unit_of_measurement': '%',
})
# ---
# name: test_setup[number.mock_classicvario_manual_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock classicVARIO Manual speed',
'max': 100.0,
'min': 0.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.mock_classicvario_manual_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[number.mock_classicvario_night_speed-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100.0,
'min': 0.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.mock_classicvario_night_speed',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Night speed',
'platform': 'eheimdigital',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'night_speed',
'unique_id': '00:00:00:00:00:03_night_speed',
'unit_of_measurement': '%',
})
# ---
# name: test_setup[number.mock_classicvario_night_speed-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Mock classicVARIO Night speed',
'max': 100.0,
'min': 0.0,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.mock_classicvario_night_speed',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[number.mock_heater_night_temperature_offset-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 5,
'min': -5,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.mock_heater_night_temperature_offset',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Night temperature offset',
'platform': 'eheimdigital',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'night_temperature_offset',
'unique_id': '00:00:00:00:00:02_night_temperature_offset',
'unit_of_measurement': None,
})
# ---
# name: test_setup[number.mock_heater_night_temperature_offset-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Mock Heater Night temperature offset',
'max': 5,
'min': -5,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.5,
}),
'context': <ANY>,
'entity_id': 'number.mock_heater_night_temperature_offset',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
# name: test_setup[number.mock_heater_temperature_offset-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 3,
'min': -3,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.mock_heater_temperature_offset',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <NumberDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature offset',
'platform': 'eheimdigital',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'temperature_offset',
'unique_id': '00:00:00:00:00:02_temperature_offset',
'unit_of_measurement': None,
})
# ---
# name: test_setup[number.mock_heater_temperature_offset-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': 'Mock Heater Temperature offset',
'max': 3,
'min': -3,
'mode': <NumberMode.AUTO: 'auto'>,
'step': 0.1,
}),
'context': <ANY>,
'entity_id': 'number.mock_heater_temperature_offset',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@ -0,0 +1,189 @@
"""Tests for the number module."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.number import (
ATTR_VALUE,
DOMAIN as NUMBER_DOMAIN,
SERVICE_SET_VALUE,
)
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import init_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("classic_vario_mock", "heater_mock")
async def test_setup(
hass: HomeAssistant,
eheimdigital_hub_mock: MagicMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test number platform setup."""
mock_config_entry.add_to_hass(hass)
with (
patch("homeassistant.components.eheimdigital.PLATFORMS", [Platform.NUMBER]),
patch(
"homeassistant.components.eheimdigital.coordinator.asyncio.Event",
new=AsyncMock,
),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
for device in eheimdigital_hub_mock.return_value.devices:
await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"](
device, eheimdigital_hub_mock.return_value.devices[device].device_type
)
await hass.async_block_till_done()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.usefixtures("classic_vario_mock", "heater_mock")
@pytest.mark.parametrize(
("device_name", "entity_list"),
[
(
"heater_mock",
[
(
"number.mock_heater_temperature_offset",
0.4,
"set_temperature_offset",
(0.4,),
),
(
"number.mock_heater_night_temperature_offset",
0.4,
"set_night_temperature_offset",
(0.4,),
),
],
),
(
"classic_vario_mock",
[
(
"number.mock_classicvario_manual_speed",
72.1,
"set_manual_speed",
(int(72.1),),
),
(
"number.mock_classicvario_day_speed",
72.1,
"set_day_speed",
(int(72.1),),
),
(
"number.mock_classicvario_night_speed",
72.1,
"set_night_speed",
(int(72.1),),
),
],
),
],
)
async def test_set_value(
hass: HomeAssistant,
eheimdigital_hub_mock: MagicMock,
mock_config_entry: MockConfigEntry,
device_name: str,
entity_list: list[tuple[str, float, str, tuple[float]]],
request: pytest.FixtureRequest,
) -> None:
"""Test setting a value."""
device: MagicMock = request.getfixturevalue(device_name)
await init_integration(hass, mock_config_entry)
await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"](
device.mac_address, device.device_type
)
await hass.async_block_till_done()
for item in entity_list:
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: item[0], ATTR_VALUE: item[1]},
blocking=True,
)
calls = [call for call in device.mock_calls if call[0] == item[2]]
assert len(calls) == 1 and calls[0][1] == item[3]
@pytest.mark.usefixtures("classic_vario_mock", "heater_mock")
@pytest.mark.parametrize(
("device_name", "entity_list"),
[
(
"heater_mock",
[
(
"number.mock_heater_temperature_offset",
"temperature_offset",
-1.1,
),
(
"number.mock_heater_night_temperature_offset",
"night_temperature_offset",
2.3,
),
],
),
(
"classic_vario_mock",
[
(
"number.mock_classicvario_manual_speed",
"manual_speed",
34,
),
(
"number.mock_classicvario_day_speed",
"day_speed",
79,
),
(
"number.mock_classicvario_night_speed",
"night_speed",
12,
),
],
),
],
)
async def test_state_update(
hass: HomeAssistant,
eheimdigital_hub_mock: MagicMock,
mock_config_entry: MockConfigEntry,
device_name: str,
entity_list: list[tuple[str, str, float]],
request: pytest.FixtureRequest,
) -> None:
"""Test state updates."""
device: MagicMock = request.getfixturevalue(device_name)
await init_integration(hass, mock_config_entry)
await eheimdigital_hub_mock.call_args.kwargs["device_found_callback"](
device.mac_address, device.device_type
)
await hass.async_block_till_done()
for item in entity_list:
setattr(device, item[1], item[2])
await eheimdigital_hub_mock.call_args.kwargs["receive_callback"]()
assert (state := hass.states.get(item[0]))
assert state.state == str(item[2])

View File

@ -1622,3 +1622,96 @@ async def test_discovery_mqtt_initiation(
assert result["result"]
assert result["result"].unique_id == "11:22:33:44:55:aa"
@pytest.mark.usefixtures("mock_zeroconf")
async def test_user_flow_name_conflict_migrate(
hass: HomeAssistant,
mock_client,
mock_setup_entry: None,
) -> None:
"""Test handle migration on name conflict."""
existing_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_DEVICE_NAME: "test"},
unique_id="11:22:33:44:55:cc",
)
existing_entry.add_to_hass(hass)
mock_client.device_info = AsyncMock(
return_value=DeviceInfo(
uses_password=False,
name="test",
mac_address="11:22:33:44:55:AA",
)
)
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "name_conflict"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "name_conflict_migrate"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "name_conflict_migrated"
assert existing_entry.data == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: "",
CONF_DEVICE_NAME: "test",
}
assert existing_entry.unique_id == "11:22:33:44:55:aa"
@pytest.mark.usefixtures("mock_zeroconf")
async def test_user_flow_name_conflict_overwrite(
hass: HomeAssistant,
mock_client,
mock_setup_entry: None,
) -> None:
"""Test handle overwrite on name conflict."""
existing_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_DEVICE_NAME: "test"},
unique_id="11:22:33:44:55:cc",
)
existing_entry.add_to_hass(hass)
mock_client.device_info = AsyncMock(
return_value=DeviceInfo(
uses_password=False,
name="test",
mac_address="11:22:33:44:55:AA",
)
)
result = await hass.config_entries.flow.async_init(
"esphome",
context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "name_conflict"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"next_step_id": "name_conflict_overwrite"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: "127.0.0.1",
CONF_PORT: 6053,
CONF_PASSWORD: "",
CONF_NOISE_PSK: "",
CONF_DEVICE_NAME: "test",
}
assert result["context"]["unique_id"] == "11:22:33:44:55:aa"

View File

@ -365,6 +365,7 @@ async def test_invalid_schemas() -> None:
{"platform": "time_pattern", "minutes": "/"},
{"platform": "time_pattern", "minutes": "*/5"},
{"platform": "time_pattern", "minutes": "/90"},
{"platform": "time_pattern", "hours": "/0", "minutes": 10},
{"platform": "time_pattern", "hours": 12, "minutes": 0, "seconds": 100},
)