mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
merge
This commit is contained in:
commit
d8f6551dab
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -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:
|
||||
|
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -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 }}
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
|
177
homeassistant/components/eheimdigital/number.py
Normal file
177
homeassistant/components/eheimdigital/number.py
Normal 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
|
||||
)
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -105,7 +105,7 @@
|
||||
"sensor": {
|
||||
"heating": {
|
||||
"state": {
|
||||
"manual": "Manual",
|
||||
"manual": "[%key:common::state::manual%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -115,7 +115,7 @@
|
||||
"state": {
|
||||
"right_handed": "Right-handed",
|
||||
"left_handed": "Left-handed",
|
||||
"auto": "Auto"
|
||||
"auto": "[%key:common::state::auto%]"
|
||||
}
|
||||
},
|
||||
"animation_speed": {
|
||||
|
@ -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%]",
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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%]"
|
||||
}
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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%]",
|
||||
|
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
92
homeassistant/components/uptimerobot/quality_scale.yaml
Normal file
92
homeassistant/components/uptimerobot/quality_scale.yaml
Normal 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
|
@ -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")]
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.146.0"]
|
||||
"requirements": ["zeroconf==0.146.5"]
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
2
requirements.txt
generated
@ -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
6
requirements_all.txt
generated
@ -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
|
||||
|
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@ -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
|
||||
|
@ -1059,7 +1059,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"upcloud",
|
||||
"upnp",
|
||||
"uptime",
|
||||
"uptimerobot",
|
||||
"usb",
|
||||
"usgs_earthquakes_feed",
|
||||
"utility_meter",
|
||||
|
@ -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
|
||||
|
286
tests/components/eheimdigital/snapshots/test_number.ambr
Normal file
286
tests/components/eheimdigital/snapshots/test_number.ambr
Normal 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',
|
||||
})
|
||||
# ---
|
189
tests/components/eheimdigital/test_number.py
Normal file
189
tests/components/eheimdigital/test_number.py
Normal 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])
|
@ -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"
|
||||
|
@ -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},
|
||||
)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user