mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +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
|
name: Report an issue with Home Assistant Core
|
||||||
description: Report an issue with Home Assistant Core.
|
description: Report an issue with Home Assistant Core.
|
||||||
|
type: Bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -1317,7 +1317,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@v5.4.0
|
uses: codecov/codecov-action@v5.4.2
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@ -1459,7 +1459,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@v5.4.0
|
uses: codecov/codecov-action@v5.4.2
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
@ -28,10 +28,10 @@
|
|||||||
"name": "Thermostat",
|
"name": "Thermostat",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
|
"auto": "[%key:common::state::auto%]",
|
||||||
"heat": "Heat",
|
"heat": "Heat",
|
||||||
"cool": "Cool",
|
"cool": "Cool",
|
||||||
"heat_cool": "Heat/Cool",
|
"heat_cool": "Heat/Cool",
|
||||||
"auto": "Auto",
|
|
||||||
"dry": "Dry",
|
"dry": "Dry",
|
||||||
"fan_only": "Fan only"
|
"fan_only": "Fan only"
|
||||||
},
|
},
|
||||||
@ -50,7 +50,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"on": "[%key:common::state::on%]",
|
"on": "[%key:common::state::on%]",
|
||||||
"auto": "Auto",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"low": "[%key:common::state::low%]",
|
"low": "[%key:common::state::low%]",
|
||||||
"medium": "[%key:common::state::medium%]",
|
"medium": "[%key:common::state::medium%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
@ -69,13 +69,13 @@
|
|||||||
"hvac_action": {
|
"hvac_action": {
|
||||||
"name": "Current action",
|
"name": "Current action",
|
||||||
"state": {
|
"state": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"idle": "[%key:common::state::idle%]",
|
||||||
"cooling": "Cooling",
|
"cooling": "Cooling",
|
||||||
"defrosting": "Defrosting",
|
"defrosting": "Defrosting",
|
||||||
"drying": "Drying",
|
"drying": "Drying",
|
||||||
"fan": "Fan",
|
"fan": "Fan",
|
||||||
"heating": "Heating",
|
"heating": "Heating",
|
||||||
"idle": "[%key:common::state::idle%]",
|
|
||||||
"off": "[%key:common::state::off%]",
|
|
||||||
"preheating": "Preheating"
|
"preheating": "Preheating"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -258,7 +258,7 @@
|
|||||||
"hvac_mode": {
|
"hvac_mode": {
|
||||||
"options": {
|
"options": {
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"auto": "Auto",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"cool": "Cool",
|
"cool": "Cool",
|
||||||
"dry": "Dry",
|
"dry": "Dry",
|
||||||
"fan_only": "Fan only",
|
"fan_only": "Fan only",
|
||||||
|
@ -75,4 +75,7 @@ class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if switch is on."""
|
"""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 .const import DOMAIN
|
||||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
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(
|
async def async_setup_entry(
|
||||||
|
@ -1,5 +1,22 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"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": {
|
"sensor": {
|
||||||
"current_speed": {
|
"current_speed": {
|
||||||
"default": "mdi:pump"
|
"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": {
|
"sensor": {
|
||||||
"current_speed": {
|
"current_speed": {
|
||||||
"name": "Current speed"
|
"name": "Current speed"
|
||||||
|
@ -47,6 +47,7 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .dashboard import async_get_or_create_dashboard_manager, async_set_dashboard_info
|
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_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||||
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
||||||
@ -74,6 +75,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
# The ESPHome name as per its config
|
# The ESPHome name as per its config
|
||||||
self._device_name: str | None = None
|
self._device_name: str | None = None
|
||||||
self._device_mac: str | None = None
|
self._device_mac: str | None = None
|
||||||
|
self._entry_with_name_conflict: ConfigEntry | None = None
|
||||||
|
|
||||||
async def _async_step_user_base(
|
async def _async_step_user_base(
|
||||||
self, user_input: dict[str, Any] | None = None, error: str | None = None
|
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."""
|
"""Handle reauthorization flow when encryption was removed."""
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self._noise_psk = None
|
self._noise_psk = None
|
||||||
return self._async_get_entry()
|
return await self._async_get_entry_or_resolve_conflict()
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_encryption_removed_confirm",
|
step_id="reauth_encryption_removed_confirm",
|
||||||
@ -227,7 +229,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return await self.async_step_authenticate()
|
return await self.async_step_authenticate()
|
||||||
|
|
||||||
self._password = ""
|
self._password = ""
|
||||||
return self._async_get_entry()
|
return await self._async_get_entry_or_resolve_conflict()
|
||||||
|
|
||||||
async def async_step_discovery_confirm(
|
async def async_step_discovery_confirm(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@ -354,6 +356,77 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
return self.async_abort(reason="service_received")
|
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
|
@callback
|
||||||
def _async_get_entry(self) -> ConfigFlowResult:
|
def _async_get_entry(self) -> ConfigFlowResult:
|
||||||
config_data = {
|
config_data = {
|
||||||
@ -407,7 +480,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
error = await self.try_login()
|
error = await self.try_login()
|
||||||
if error:
|
if error:
|
||||||
return await self.async_step_authenticate(error=error)
|
return await self.async_step_authenticate(error=error)
|
||||||
return self._async_get_entry()
|
return await self._async_get_entry_or_resolve_conflict()
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
if error is not None:
|
if error is not None:
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
|
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
|
||||||
"mqtt_missing_api": "Missing API port in MQTT properties.",
|
"mqtt_missing_api": "Missing API port in MQTT properties.",
|
||||||
"mqtt_missing_ip": "Missing IP address 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": {
|
"error": {
|
||||||
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address",
|
"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,
|
FibaroClient,
|
||||||
FibaroConnectFailed,
|
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 import DeviceModel
|
||||||
from pyfibaro.fibaro_device_manager import FibaroDeviceManager
|
from pyfibaro.fibaro_device_manager import FibaroDeviceManager
|
||||||
from pyfibaro.fibaro_info import InfoModel
|
from pyfibaro.fibaro_info import InfoModel
|
||||||
@ -176,35 +176,18 @@ class FibaroController:
|
|||||||
platform = Platform.LIGHT
|
platform = Platform.LIGHT
|
||||||
return platform
|
return platform
|
||||||
|
|
||||||
def _create_device_info(
|
def _create_device_info(self, main_device: DeviceModel) -> None:
|
||||||
self, device: DeviceModel, devices: list[DeviceModel]
|
"""Create the device info for a main device."""
|
||||||
) -> None:
|
|
||||||
"""Create the device info. Unrooted entities are directly shown below the home center."""
|
|
||||||
|
|
||||||
# The home center is always id 1 (z-wave primary controller)
|
if "zwaveCompany" in main_device.properties:
|
||||||
if device.parent_fibaro_id <= 1:
|
manufacturer = main_device.properties.get("zwaveCompany")
|
||||||
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")
|
|
||||||
else:
|
else:
|
||||||
manufacturer = None
|
manufacturer = None
|
||||||
|
|
||||||
self._device_infos[master_entity.fibaro_id] = DeviceInfo(
|
self._device_infos[main_device.fibaro_id] = DeviceInfo(
|
||||||
identifiers={(DOMAIN, master_entity.fibaro_id)},
|
identifiers={(DOMAIN, main_device.fibaro_id)},
|
||||||
manufacturer=manufacturer,
|
manufacturer=manufacturer,
|
||||||
name=master_entity.name,
|
name=main_device.name,
|
||||||
via_device=(DOMAIN, self.hub_serial),
|
via_device=(DOMAIN, self.hub_serial),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -239,6 +222,10 @@ class FibaroController:
|
|||||||
def _read_devices(self) -> None:
|
def _read_devices(self) -> None:
|
||||||
"""Read and process the device list."""
|
"""Read and process the device list."""
|
||||||
devices = self._fibaro_device_manager.get_devices()
|
devices = self._fibaro_device_manager.get_devices()
|
||||||
|
|
||||||
|
for main_device in find_master_devices(devices):
|
||||||
|
self._create_device_info(main_device)
|
||||||
|
|
||||||
self._device_map = {}
|
self._device_map = {}
|
||||||
last_climate_parent = None
|
last_climate_parent = None
|
||||||
last_endpoint = None
|
last_endpoint = None
|
||||||
@ -258,7 +245,6 @@ class FibaroController:
|
|||||||
if platform is None:
|
if platform is None:
|
||||||
continue
|
continue
|
||||||
device.unique_id_str = f"{slugify(self.hub_serial)}.{device.fibaro_id}"
|
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
|
self._device_map[device.fibaro_id] = device
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s (%s, %s) -> %s %s",
|
"%s (%s, %s) -> %s %s",
|
||||||
|
@ -105,7 +105,7 @@
|
|||||||
"sensor": {
|
"sensor": {
|
||||||
"heating": {
|
"heating": {
|
||||||
"state": {
|
"state": {
|
||||||
"manual": "Manual",
|
"manual": "[%key:common::state::manual%]",
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"schedule": "Schedule"
|
"schedule": "Schedule"
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,8 @@ class TimePattern:
|
|||||||
|
|
||||||
if isinstance(value, str) and value.startswith("/"):
|
if isinstance(value, str) and value.startswith("/"):
|
||||||
number = int(value[1:])
|
number = int(value[1:])
|
||||||
|
if number == 0:
|
||||||
|
raise vol.Invalid(f"must be a value between 1 and {self.maximum}")
|
||||||
else:
|
else:
|
||||||
value = number = int(value)
|
value = number = int(value)
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"right_handed": "Right-handed",
|
"right_handed": "Right-handed",
|
||||||
"left_handed": "Left-handed",
|
"left_handed": "Left-handed",
|
||||||
"auto": "Auto"
|
"auto": "[%key:common::state::auto%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"animation_speed": {
|
"animation_speed": {
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
"mid": "[%key:common::state::medium%]",
|
"mid": "[%key:common::state::medium%]",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::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": {
|
"preset_mode": {
|
||||||
@ -343,7 +343,7 @@
|
|||||||
"growth_mode": {
|
"growth_mode": {
|
||||||
"name": "Mode",
|
"name": "Mode",
|
||||||
"state": {
|
"state": {
|
||||||
"standard": "Auto",
|
"standard": "[%key:common::state::auto%]",
|
||||||
"ext_leaf": "Vegetables",
|
"ext_leaf": "Vegetables",
|
||||||
"ext_herb": "Herbs",
|
"ext_herb": "Herbs",
|
||||||
"ext_flower": "Flowers",
|
"ext_flower": "Flowers",
|
||||||
@ -353,7 +353,7 @@
|
|||||||
"growth_mode_for_location": {
|
"growth_mode_for_location": {
|
||||||
"name": "{location} mode",
|
"name": "{location} mode",
|
||||||
"state": {
|
"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_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_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%]",
|
"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%]",
|
"name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"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%]",
|
"power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
|
||||||
"replace": "Replace filter",
|
"replace": "Replace filter",
|
||||||
"smart_power": "Smart safe storage",
|
"smart_power": "Smart safe storage",
|
||||||
@ -599,7 +599,7 @@
|
|||||||
"name": "Operating mode",
|
"name": "Operating mode",
|
||||||
"state": {
|
"state": {
|
||||||
"air_clean": "Purify",
|
"air_clean": "Purify",
|
||||||
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"clothes_dry": "Laundry",
|
"clothes_dry": "Laundry",
|
||||||
"edge": "Edge cleaning",
|
"edge": "Edge cleaning",
|
||||||
"heat_pump": "Heat pump",
|
"heat_pump": "Heat pump",
|
||||||
@ -649,7 +649,7 @@
|
|||||||
"current_dish_washing_course": {
|
"current_dish_washing_course": {
|
||||||
"name": "Current cycle",
|
"name": "Current cycle",
|
||||||
"state": {
|
"state": {
|
||||||
"auto": "[%key:component::lg_thinq::entity::sensor::growth_mode::state::standard%]",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"heavy": "Intensive",
|
"heavy": "Intensive",
|
||||||
"delicate": "Delicate",
|
"delicate": "Delicate",
|
||||||
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
|
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
|
||||||
@ -881,7 +881,7 @@
|
|||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"power": "Turbo",
|
"power": "Turbo",
|
||||||
"turbo": "[%key:component::lg_thinq::entity::select::wind_strength::state::power%]",
|
"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_1": "Step 1",
|
||||||
"wind_2": "Step 2",
|
"wind_2": "Step 2",
|
||||||
"wind_3": "Step 3",
|
"wind_3": "Step 3",
|
||||||
@ -905,7 +905,7 @@
|
|||||||
"name": "Operating mode",
|
"name": "Operating mode",
|
||||||
"state": {
|
"state": {
|
||||||
"air_clean": "Purifying",
|
"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%]",
|
"baby_care": "[%key:component::lg_thinq::entity::sensor::personalization_mode::state::baby%]",
|
||||||
"circulator": "Booster",
|
"circulator": "Booster",
|
||||||
"clean": "Single",
|
"clean": "Single",
|
||||||
@ -1016,7 +1016,7 @@
|
|||||||
"name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]",
|
"name": "[%key:component::lg_thinq::entity::binary_sensor::one_touch_filter::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"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%]",
|
"power": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::high%]",
|
||||||
"replace": "[%key:component::lg_thinq::entity::sensor::fresh_air_filter::state::replace%]",
|
"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%]",
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/lutron",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pylutron"],
|
"loggers": ["pylutron"],
|
||||||
"requirements": ["pylutron==0.2.16"],
|
"requirements": ["pylutron==0.2.18"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"issues": {
|
"issues": {
|
||||||
"invalid_platform_config": {
|
"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."
|
"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": {
|
"invalid_unit_of_measurement": {
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"title": "Starting add-on"
|
"title": "Starting add-on"
|
||||||
},
|
},
|
||||||
"hassio_confirm": {
|
"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}?"
|
"description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the add-on {addon}?"
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
@ -153,7 +153,7 @@
|
|||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"mqtt_settings": {
|
"mqtt_settings": {
|
||||||
"name": "MQTT Settings",
|
"name": "MQTT settings",
|
||||||
"data": {
|
"data": {
|
||||||
"qos": "QoS"
|
"qos": "QoS"
|
||||||
},
|
},
|
||||||
@ -480,7 +480,7 @@
|
|||||||
"set_ca_cert": {
|
"set_ca_cert": {
|
||||||
"options": {
|
"options": {
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"auto": "Auto",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"custom": "Custom"
|
"custom": "Custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -71,14 +71,14 @@
|
|||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"preset_mode": {
|
"preset_mode": {
|
||||||
"state": {
|
"state": {
|
||||||
"auto": "Auto",
|
"auto": "[%key:common::state::auto%]",
|
||||||
|
"manual": "[%key:common::state::manual%]",
|
||||||
"comfort-1": "Comfort 1",
|
"comfort-1": "Comfort 1",
|
||||||
"comfort-2": "Comfort 2",
|
"comfort-2": "Comfort 2",
|
||||||
"drying": "Drying",
|
"drying": "Drying",
|
||||||
"external": "External",
|
"external": "External",
|
||||||
"freeze": "Freeze",
|
"freeze": "Freeze",
|
||||||
"frost_protection": "Frost protection",
|
"frost_protection": "Frost protection",
|
||||||
"manual": "Manual",
|
|
||||||
"night": "Night",
|
"night": "Night",
|
||||||
"prog": "Prog"
|
"prog": "Prog"
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"password": "The Smile ID printed on the label on the back of your Adam, Smile-T, or P1.",
|
"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.",
|
"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."
|
"username": "Default is `smile`, or `stretch` for the legacy Stretch."
|
||||||
}
|
}
|
||||||
@ -113,7 +113,7 @@
|
|||||||
"name": "DHW mode",
|
"name": "DHW mode",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"auto": "Auto",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
|
"boost": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::boost%]",
|
||||||
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]"
|
"comfort": "[%key:component::climate::entity_component::_::state_attributes::preset_mode::state::comfort%]"
|
||||||
}
|
}
|
||||||
|
@ -19,5 +19,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["reolink-aio==0.13.1"]
|
"requirements": ["reolink-aio==0.13.2"]
|
||||||
}
|
}
|
||||||
|
@ -652,7 +652,7 @@
|
|||||||
"name": "Floodlight mode",
|
"name": "Floodlight mode",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"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",
|
"onatnight": "On at night",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"adaptive": "Adaptive",
|
"adaptive": "Adaptive",
|
||||||
@ -662,7 +662,7 @@
|
|||||||
"day_night_mode": {
|
"day_night_mode": {
|
||||||
"name": "Day night mode",
|
"name": "Day night mode",
|
||||||
"state": {
|
"state": {
|
||||||
"auto": "Auto",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"color": "Color",
|
"color": "Color",
|
||||||
"blackwhite": "Black & white"
|
"blackwhite": "Black & white"
|
||||||
}
|
}
|
||||||
@ -691,7 +691,7 @@
|
|||||||
"name": "Doorbell LED",
|
"name": "Doorbell LED",
|
||||||
"state": {
|
"state": {
|
||||||
"stayoff": "Stay off",
|
"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",
|
"alwaysonatnight": "Auto & always on at night",
|
||||||
"always": "Always on",
|
"always": "Always on",
|
||||||
"alwayson": "Always on"
|
"alwayson": "Always on"
|
||||||
@ -702,7 +702,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"on": "[%key:common::state::on%]",
|
"on": "[%key:common::state::on%]",
|
||||||
"auto": "[%key:component::reolink::entity::select::day_night_mode::state::auto%]"
|
"auto": "[%key:common::state::auto%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"binning_mode": {
|
"binning_mode": {
|
||||||
@ -710,7 +710,7 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
"on": "[%key:common::state::on%]",
|
"on": "[%key:common::state::on%]",
|
||||||
"auto": "[%key:component::reolink::entity::select::day_night_mode::state::auto%]"
|
"auto": "[%key:common::state::auto%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"hub_alarm_ringtone": {
|
"hub_alarm_ringtone": {
|
||||||
|
@ -426,11 +426,11 @@
|
|||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"fan_speed": {
|
"fan_speed": {
|
||||||
"state": {
|
"state": {
|
||||||
"auto": "Auto",
|
"off": "[%key:common::state::off%]",
|
||||||
|
"auto": "[%key:common::state::auto%]",
|
||||||
"balanced": "Balanced",
|
"balanced": "Balanced",
|
||||||
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
||||||
"gentle": "Gentle",
|
"gentle": "Gentle",
|
||||||
"off": "[%key:common::state::off%]",
|
|
||||||
"max": "[%key:component::roborock::entity::select::mop_intensity::state::max%]",
|
"max": "[%key:component::roborock::entity::select::mop_intensity::state::max%]",
|
||||||
"max_plus": "Max plus",
|
"max_plus": "Max plus",
|
||||||
"medium": "[%key:common::state::medium%]",
|
"medium": "[%key:common::state::medium%]",
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"preset_mode": {
|
"preset_mode": {
|
||||||
"state": {
|
"state": {
|
||||||
"auto": "Auto"
|
"auto": "[%key:common::state::auto%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@
|
|||||||
"description": "Adds a meter reading to Tado Energy IQ.",
|
"description": "Adds a meter reading to Tado Energy IQ.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"config_entry": {
|
"config_entry": {
|
||||||
"name": "Config Entry",
|
"name": "Config entry",
|
||||||
"description": "Config entry to add meter reading to."
|
"description": "Config entry to add meter reading to."
|
||||||
},
|
},
|
||||||
"reading": {
|
"reading": {
|
||||||
|
@ -288,9 +288,9 @@
|
|||||||
"motion_sensitivity": {
|
"motion_sensitivity": {
|
||||||
"name": "Motion detection sensitivity",
|
"name": "Motion detection sensitivity",
|
||||||
"state": {
|
"state": {
|
||||||
"0": "Low sensitivity",
|
"0": "[%key:common::state::low%]",
|
||||||
"1": "Medium sensitivity",
|
"1": "[%key:common::state::medium%]",
|
||||||
"2": "High sensitivity"
|
"2": "[%key:common::state::high%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"record_mode": {
|
"record_mode": {
|
||||||
@ -404,7 +404,7 @@
|
|||||||
"humidifier_spray_mode": {
|
"humidifier_spray_mode": {
|
||||||
"name": "Spray mode",
|
"name": "Spray mode",
|
||||||
"state": {
|
"state": {
|
||||||
"auto": "Auto",
|
"auto": "[%key:common::state::auto%]",
|
||||||
"health": "Health",
|
"health": "Health",
|
||||||
"sleep": "Sleep",
|
"sleep": "Sleep",
|
||||||
"humidity": "Humidity",
|
"humidity": "Humidity",
|
||||||
|
@ -43,4 +43,4 @@ class UptimeRobotBinarySensor(UptimeRobotEntity, BinarySensorEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if the entity is on."""
|
"""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,
|
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:
|
async def async_setup_entry(hass: HomeAssistant, entry: WhirlpoolConfigEntry) -> bool:
|
||||||
"""Set up Whirlpool Sixth Sense from a config entry."""
|
"""Set up Whirlpool Sixth Sense from a config entry."""
|
||||||
hass.data.setdefault(DOMAIN, {})
|
|
||||||
|
|
||||||
session = async_get_clientsession(hass)
|
session = async_get_clientsession(hass)
|
||||||
region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")]
|
region = CONF_REGIONS_MAP[entry.data.get(CONF_REGION, "EU")]
|
||||||
brand = CONF_BRANDS_MAP[entry.data.get(CONF_BRAND, "Whirlpool")]
|
brand = CONF_BRANDS_MAP[entry.data.get(CONF_BRAND, "Whirlpool")]
|
||||||
|
@ -8,5 +8,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["zeroconf"],
|
"loggers": ["zeroconf"],
|
||||||
"quality_scale": "internal",
|
"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
|
voluptuous==0.15.2
|
||||||
webrtc-models==0.3.0
|
webrtc-models==0.3.0
|
||||||
yarl==1.19.0
|
yarl==1.19.0
|
||||||
zeroconf==0.146.0
|
zeroconf==0.146.5
|
||||||
|
|
||||||
# Constrain pycryptodome to avoid vulnerability
|
# Constrain pycryptodome to avoid vulnerability
|
||||||
# see https://github.com/home-assistant/core/pull/16238
|
# see https://github.com/home-assistant/core/pull/16238
|
||||||
|
@ -123,7 +123,7 @@ dependencies = [
|
|||||||
"voluptuous-openapi==0.0.6",
|
"voluptuous-openapi==0.0.6",
|
||||||
"yarl==1.19.0",
|
"yarl==1.19.0",
|
||||||
"webrtc-models==0.3.0",
|
"webrtc-models==0.3.0",
|
||||||
"zeroconf==0.146.0",
|
"zeroconf==0.146.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@ -60,4 +60,4 @@ voluptuous-serialize==2.6.0
|
|||||||
voluptuous-openapi==0.0.6
|
voluptuous-openapi==0.0.6
|
||||||
yarl==1.19.0
|
yarl==1.19.0
|
||||||
webrtc-models==0.3.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
|
pylutron-caseta==0.24.0
|
||||||
|
|
||||||
# homeassistant.components.lutron
|
# homeassistant.components.lutron
|
||||||
pylutron==0.2.16
|
pylutron==0.2.18
|
||||||
|
|
||||||
# homeassistant.components.mailgun
|
# homeassistant.components.mailgun
|
||||||
pymailgunner==1.4
|
pymailgunner==1.4
|
||||||
@ -2633,7 +2633,7 @@ renault-api==0.2.9
|
|||||||
renson-endura-delta==1.7.2
|
renson-endura-delta==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.13.1
|
reolink-aio==0.13.2
|
||||||
|
|
||||||
# homeassistant.components.idteck_prox
|
# homeassistant.components.idteck_prox
|
||||||
rfk101py==0.0.1
|
rfk101py==0.0.1
|
||||||
@ -3152,7 +3152,7 @@ zabbix-utils==2.0.2
|
|||||||
zamg==0.3.6
|
zamg==0.3.6
|
||||||
|
|
||||||
# homeassistant.components.zeroconf
|
# homeassistant.components.zeroconf
|
||||||
zeroconf==0.146.0
|
zeroconf==0.146.5
|
||||||
|
|
||||||
# homeassistant.components.zeversolar
|
# homeassistant.components.zeversolar
|
||||||
zeversolar==0.3.2
|
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
|
pylutron-caseta==0.24.0
|
||||||
|
|
||||||
# homeassistant.components.lutron
|
# homeassistant.components.lutron
|
||||||
pylutron==0.2.16
|
pylutron==0.2.18
|
||||||
|
|
||||||
# homeassistant.components.mailgun
|
# homeassistant.components.mailgun
|
||||||
pymailgunner==1.4
|
pymailgunner==1.4
|
||||||
@ -2137,7 +2137,7 @@ renault-api==0.2.9
|
|||||||
renson-endura-delta==1.7.2
|
renson-endura-delta==1.7.2
|
||||||
|
|
||||||
# homeassistant.components.reolink
|
# homeassistant.components.reolink
|
||||||
reolink-aio==0.13.1
|
reolink-aio==0.13.2
|
||||||
|
|
||||||
# homeassistant.components.rflink
|
# homeassistant.components.rflink
|
||||||
rflink==0.0.66
|
rflink==0.0.66
|
||||||
@ -2548,7 +2548,7 @@ yt-dlp[default]==2025.03.26
|
|||||||
zamg==0.3.6
|
zamg==0.3.6
|
||||||
|
|
||||||
# homeassistant.components.zeroconf
|
# homeassistant.components.zeroconf
|
||||||
zeroconf==0.146.0
|
zeroconf==0.146.5
|
||||||
|
|
||||||
# homeassistant.components.zeversolar
|
# homeassistant.components.zeversolar
|
||||||
zeversolar==0.3.2
|
zeversolar==0.3.2
|
||||||
|
@ -1059,7 +1059,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
|||||||
"upcloud",
|
"upcloud",
|
||||||
"upnp",
|
"upnp",
|
||||||
"uptime",
|
"uptime",
|
||||||
"uptimerobot",
|
|
||||||
"usb",
|
"usb",
|
||||||
"usgs_earthquakes_feed",
|
"usgs_earthquakes_feed",
|
||||||
"utility_meter",
|
"utility_meter",
|
||||||
|
@ -61,6 +61,8 @@ def heater_mock():
|
|||||||
heater_mock.temperature_unit = HeaterUnit.CELSIUS
|
heater_mock.temperature_unit = HeaterUnit.CELSIUS
|
||||||
heater_mock.current_temperature = 24.2
|
heater_mock.current_temperature = 24.2
|
||||||
heater_mock.target_temperature = 25.5
|
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_heating = True
|
||||||
heater_mock.is_active = True
|
heater_mock.is_active = True
|
||||||
heater_mock.operation_mode = HeaterMode.MANUAL
|
heater_mock.operation_mode = HeaterMode.MANUAL
|
||||||
@ -77,6 +79,9 @@ def classic_vario_mock():
|
|||||||
classic_vario_mock.aquarium_name = "Mock Aquarium"
|
classic_vario_mock.aquarium_name = "Mock Aquarium"
|
||||||
classic_vario_mock.sw_version = "1.0.0_1.0.0"
|
classic_vario_mock.sw_version = "1.0.0_1.0.0"
|
||||||
classic_vario_mock.current_speed = 75
|
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.is_active = True
|
||||||
classic_vario_mock.filter_mode = FilterMode.MANUAL
|
classic_vario_mock.filter_mode = FilterMode.MANUAL
|
||||||
classic_vario_mock.error_code = FilterErrorCode.NO_ERROR
|
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"]
|
||||||
assert result["result"].unique_id == "11:22:33:44:55:aa"
|
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": "/"},
|
||||||
{"platform": "time_pattern", "minutes": "*/5"},
|
{"platform": "time_pattern", "minutes": "*/5"},
|
||||||
{"platform": "time_pattern", "minutes": "/90"},
|
{"platform": "time_pattern", "minutes": "/90"},
|
||||||
|
{"platform": "time_pattern", "hours": "/0", "minutes": 10},
|
||||||
{"platform": "time_pattern", "hours": 12, "minutes": 0, "seconds": 100},
|
{"platform": "time_pattern", "hours": 12, "minutes": 0, "seconds": 100},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user