mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add support for controlling manual watering time on Melnor Bluetooth devices (#78653)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
3dc6db94ce
commit
3eab4a234b
@ -15,6 +15,7 @@ from .const import DOMAIN
|
|||||||
from .models import MelnorDataUpdateCoordinator
|
from .models import MelnorDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
|
Platform.NUMBER,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
"""Melnor integration models."""
|
"""Melnor integration models."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TypeVar
|
||||||
|
|
||||||
from melnor_bluetooth.device import Device, Valve
|
from melnor_bluetooth.device import Device, Valve
|
||||||
|
|
||||||
|
from homeassistant.components.number import EntityDescription
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import (
|
from homeassistant.helpers.update_coordinator import (
|
||||||
@ -39,7 +42,7 @@ class MelnorDataUpdateCoordinator(DataUpdateCoordinator[Device]):
|
|||||||
return self._device
|
return self._device
|
||||||
|
|
||||||
|
|
||||||
class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]):
|
class MelnorBluetoothEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]):
|
||||||
"""Base class for melnor entities."""
|
"""Base class for melnor entities."""
|
||||||
|
|
||||||
_device: Device
|
_device: Device
|
||||||
@ -73,7 +76,7 @@ class MelnorBluetoothBaseEntity(CoordinatorEntity[MelnorDataUpdateCoordinator]):
|
|||||||
return self._device.is_connected
|
return self._device.is_connected
|
||||||
|
|
||||||
|
|
||||||
class MelnorZoneEntity(MelnorBluetoothBaseEntity):
|
class MelnorZoneEntity(MelnorBluetoothEntity):
|
||||||
"""Base class for valves that define themselves as child devices."""
|
"""Base class for valves that define themselves as child devices."""
|
||||||
|
|
||||||
_valve: Valve
|
_valve: Valve
|
||||||
@ -81,11 +84,17 @@ class MelnorZoneEntity(MelnorBluetoothBaseEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: MelnorDataUpdateCoordinator,
|
coordinator: MelnorDataUpdateCoordinator,
|
||||||
|
entity_description: EntityDescription,
|
||||||
valve: Valve,
|
valve: Valve,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a valve entity."""
|
"""Initialize a valve entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self._attr_unique_id = (
|
||||||
|
f"{self._device.mac}-zone{valve.id}-{entity_description.key}"
|
||||||
|
)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
|
||||||
self._valve = valve
|
self._valve = valve
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
@ -94,3 +103,28 @@ class MelnorZoneEntity(MelnorBluetoothBaseEntity):
|
|||||||
name=f"Zone {valve.id + 1}",
|
name=f"Zone {valve.id + 1}",
|
||||||
via_device=(DOMAIN, self._device.mac),
|
via_device=(DOMAIN, self._device.mac),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T", bound=EntityDescription)
|
||||||
|
|
||||||
|
|
||||||
|
def get_entities_for_valves(
|
||||||
|
coordinator: MelnorDataUpdateCoordinator,
|
||||||
|
descriptions: list[T],
|
||||||
|
function: Callable[
|
||||||
|
[Valve, T],
|
||||||
|
CoordinatorEntity[MelnorDataUpdateCoordinator],
|
||||||
|
],
|
||||||
|
) -> list[CoordinatorEntity[MelnorDataUpdateCoordinator]]:
|
||||||
|
"""Get descriptions for valves."""
|
||||||
|
entities = []
|
||||||
|
|
||||||
|
# This device may not have 4 valves total, but the library will only expose the right number of valves
|
||||||
|
for i in range(1, 5):
|
||||||
|
valve = coordinator.data[f"zone{i}"]
|
||||||
|
|
||||||
|
if valve is not None:
|
||||||
|
for description in descriptions:
|
||||||
|
entities.append(function(valve, description))
|
||||||
|
|
||||||
|
return entities
|
||||||
|
96
homeassistant/components/melnor/number.py
Normal file
96
homeassistant/components/melnor/number.py
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
"""Number support for Melnor Bluetooth water timer."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from melnor_bluetooth.device import Valve
|
||||||
|
|
||||||
|
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .models import (
|
||||||
|
MelnorDataUpdateCoordinator,
|
||||||
|
MelnorZoneEntity,
|
||||||
|
get_entities_for_valves,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MelnorZoneNumberEntityDescriptionMixin:
|
||||||
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
|
set_num_fn: Callable[[Valve, int], Coroutine[Any, Any, None]]
|
||||||
|
state_fn: Callable[[Valve], Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MelnorZoneNumberEntityDescription(
|
||||||
|
NumberEntityDescription, MelnorZoneNumberEntityDescriptionMixin
|
||||||
|
):
|
||||||
|
"""Describes Melnor number entity."""
|
||||||
|
|
||||||
|
|
||||||
|
ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneNumberEntityDescription] = [
|
||||||
|
MelnorZoneNumberEntityDescription(
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_max_value=360,
|
||||||
|
native_min_value=1,
|
||||||
|
icon="mdi:timer-cog-outline",
|
||||||
|
key="manual_minutes",
|
||||||
|
name="Manual Minutes",
|
||||||
|
set_num_fn=lambda valve, value: valve.set_manual_watering_minutes(value),
|
||||||
|
state_fn=lambda valve: valve.manual_watering_minutes,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the number platform."""
|
||||||
|
|
||||||
|
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
get_entities_for_valves(
|
||||||
|
coordinator,
|
||||||
|
ZONE_ENTITY_DESCRIPTIONS,
|
||||||
|
lambda valve, description: MelnorZoneNumber(
|
||||||
|
coordinator, description, valve
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MelnorZoneNumber(MelnorZoneEntity, NumberEntity):
|
||||||
|
"""A number implementation for a melnor device."""
|
||||||
|
|
||||||
|
entity_description: MelnorZoneNumberEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: MelnorDataUpdateCoordinator,
|
||||||
|
entity_description: MelnorZoneNumberEntityDescription,
|
||||||
|
valve: Valve,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a number for a melnor device."""
|
||||||
|
super().__init__(coordinator, entity_description, valve)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
"""Return the current value."""
|
||||||
|
return self._valve.manual_watering_minutes
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
"""Update the current value."""
|
||||||
|
await self.entity_description.set_num_fn(self._valve, int(value))
|
||||||
|
self._async_write_ha_state()
|
@ -3,9 +3,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from melnor_bluetooth.device import Device
|
from melnor_bluetooth.device import Device, Valve
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@ -19,9 +20,26 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .models import MelnorBluetoothBaseEntity, MelnorDataUpdateCoordinator
|
from .models import (
|
||||||
|
MelnorBluetoothEntity,
|
||||||
|
MelnorDataUpdateCoordinator,
|
||||||
|
MelnorZoneEntity,
|
||||||
|
get_entities_for_valves,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def watering_seconds_left(valve: Valve) -> datetime | None:
|
||||||
|
"""Calculate the number of minutes left in the current watering cycle."""
|
||||||
|
|
||||||
|
if valve.is_watering is not True or dt_util.now() > dt_util.utc_from_timestamp(
|
||||||
|
valve.watering_end_time
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return dt_util.utc_from_timestamp(valve.watering_end_time)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -31,6 +49,20 @@ class MelnorSensorEntityDescriptionMixin:
|
|||||||
state_fn: Callable[[Device], Any]
|
state_fn: Callable[[Device], Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MelnorZoneSensorEntityDescriptionMixin:
|
||||||
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
|
state_fn: Callable[[Valve], Any]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MelnorZoneSensorEntityDescription(
|
||||||
|
SensorEntityDescription, MelnorZoneSensorEntityDescriptionMixin
|
||||||
|
):
|
||||||
|
"""Describes Melnor sensor entity."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MelnorSensorEntityDescription(
|
class MelnorSensorEntityDescription(
|
||||||
SensorEntityDescription, MelnorSensorEntityDescriptionMixin
|
SensorEntityDescription, MelnorSensorEntityDescriptionMixin
|
||||||
@ -38,7 +70,7 @@ class MelnorSensorEntityDescription(
|
|||||||
"""Describes Melnor sensor entity."""
|
"""Describes Melnor sensor entity."""
|
||||||
|
|
||||||
|
|
||||||
sensors = [
|
DEVICE_ENTITY_DESCRIPTIONS: list[MelnorSensorEntityDescription] = [
|
||||||
MelnorSensorEntityDescription(
|
MelnorSensorEntityDescription(
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
@ -60,6 +92,15 @@ sensors = [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ZONE_ENTITY_DESCRIPTIONS: list[MelnorZoneSensorEntityDescription] = [
|
||||||
|
MelnorZoneSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
key="manual_cycle_end",
|
||||||
|
name="Manual Cycle End",
|
||||||
|
state_fn=watering_seconds_left,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -70,16 +111,28 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
# Device-level sensors
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
MelnorSensorEntity(
|
MelnorSensorEntity(
|
||||||
coordinator,
|
coordinator,
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
for description in sensors
|
for description in DEVICE_ENTITY_DESCRIPTIONS
|
||||||
|
)
|
||||||
|
|
||||||
|
# Valve/Zone-level sensors
|
||||||
|
async_add_entities(
|
||||||
|
get_entities_for_valves(
|
||||||
|
coordinator,
|
||||||
|
ZONE_ENTITY_DESCRIPTIONS,
|
||||||
|
lambda valve, description: MelnorZoneSensorEntity(
|
||||||
|
coordinator, description, valve
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MelnorSensorEntity(MelnorBluetoothBaseEntity, SensorEntity):
|
class MelnorSensorEntity(MelnorBluetoothEntity, SensorEntity):
|
||||||
"""Representation of a Melnor sensor."""
|
"""Representation of a Melnor sensor."""
|
||||||
|
|
||||||
entity_description: MelnorSensorEntityDescription
|
entity_description: MelnorSensorEntityDescription
|
||||||
@ -98,5 +151,25 @@ class MelnorSensorEntity(MelnorBluetoothBaseEntity, SensorEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the battery level."""
|
"""Return the sensor value."""
|
||||||
return self.entity_description.state_fn(self._device)
|
return self.entity_description.state_fn(self._device)
|
||||||
|
|
||||||
|
|
||||||
|
class MelnorZoneSensorEntity(MelnorZoneEntity, SensorEntity):
|
||||||
|
"""Representation of a Melnor sensor."""
|
||||||
|
|
||||||
|
entity_description: MelnorZoneSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: MelnorDataUpdateCoordinator,
|
||||||
|
entity_description: MelnorZoneSensorEntityDescription,
|
||||||
|
valve: Valve,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a sensor for a Melnor device."""
|
||||||
|
super().__init__(coordinator, entity_description, valve)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the sensor value."""
|
||||||
|
return self.entity_description.state_fn(self._valve)
|
||||||
|
@ -18,7 +18,11 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .models import MelnorDataUpdateCoordinator, MelnorZoneEntity
|
from .models import (
|
||||||
|
MelnorDataUpdateCoordinator,
|
||||||
|
MelnorZoneEntity,
|
||||||
|
get_entities_for_valves,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -36,12 +40,11 @@ class MelnorSwitchEntityDescription(
|
|||||||
"""Describes Melnor switch entity."""
|
"""Describes Melnor switch entity."""
|
||||||
|
|
||||||
|
|
||||||
switches = [
|
ZONE_ENTITY_DESCRIPTIONS = [
|
||||||
MelnorSwitchEntityDescription(
|
MelnorSwitchEntityDescription(
|
||||||
device_class=SwitchDeviceClass.SWITCH,
|
device_class=SwitchDeviceClass.SWITCH,
|
||||||
icon="mdi:sprinkler",
|
icon="mdi:sprinkler",
|
||||||
key="manual",
|
key="manual",
|
||||||
name="Manual",
|
|
||||||
on_off_fn=lambda valve, bool: valve.set_is_watering(bool),
|
on_off_fn=lambda valve, bool: valve.set_is_watering(bool),
|
||||||
state_fn=lambda valve: valve.is_watering,
|
state_fn=lambda valve: valve.is_watering,
|
||||||
)
|
)
|
||||||
@ -51,22 +54,21 @@ switches = [
|
|||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
async_add_devices: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the switch platform."""
|
"""Set up the switch platform."""
|
||||||
entities: list[MelnorZoneSwitch] = []
|
|
||||||
|
|
||||||
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
coordinator: MelnorDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
# This device may not have 4 valves total, but the library will only expose the right number of valves
|
async_add_entities(
|
||||||
for i in range(1, 5):
|
get_entities_for_valves(
|
||||||
valve = coordinator.data[f"zone{i}"]
|
coordinator,
|
||||||
if valve is not None:
|
ZONE_ENTITY_DESCRIPTIONS,
|
||||||
|
lambda valve, description: MelnorZoneSwitch(
|
||||||
for description in switches:
|
coordinator, description, valve
|
||||||
entities.append(MelnorZoneSwitch(coordinator, valve, description))
|
),
|
||||||
|
)
|
||||||
async_add_devices(entities)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity):
|
class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity):
|
||||||
@ -77,14 +79,11 @@ class MelnorZoneSwitch(MelnorZoneEntity, SwitchEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: MelnorDataUpdateCoordinator,
|
coordinator: MelnorDataUpdateCoordinator,
|
||||||
valve: Valve,
|
|
||||||
entity_description: MelnorSwitchEntityDescription,
|
entity_description: MelnorSwitchEntityDescription,
|
||||||
|
valve: Valve,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a switch for a melnor device."""
|
"""Initialize a switch for a melnor device."""
|
||||||
super().__init__(coordinator, valve)
|
super().__init__(coordinator, entity_description, valve)
|
||||||
|
|
||||||
self._attr_unique_id = f"{self._device.mac}-zone{valve.id}-manual"
|
|
||||||
self.entity_description = entity_description
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
|
@ -58,9 +58,11 @@ class MockedValve:
|
|||||||
_id: int
|
_id: int
|
||||||
_is_watering: bool
|
_is_watering: bool
|
||||||
_manual_watering_minutes: int
|
_manual_watering_minutes: int
|
||||||
|
_end_time: int
|
||||||
|
|
||||||
def __init__(self, identifier: int) -> None:
|
def __init__(self, identifier: int) -> None:
|
||||||
"""Initialize a mocked valve."""
|
"""Initialize a mocked valve."""
|
||||||
|
self._end_time = 0
|
||||||
self._id = identifier
|
self._id = identifier
|
||||||
self._is_watering = False
|
self._is_watering = False
|
||||||
self._manual_watering_minutes = 0
|
self._manual_watering_minutes = 0
|
||||||
@ -79,6 +81,20 @@ class MockedValve:
|
|||||||
"""Set the valve to manual watering."""
|
"""Set the valve to manual watering."""
|
||||||
self._is_watering = is_watering
|
self._is_watering = is_watering
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manual_watering_minutes(self):
|
||||||
|
"""Return the number of minutes the valve is set to manual watering."""
|
||||||
|
return self._manual_watering_minutes
|
||||||
|
|
||||||
|
async def set_manual_watering_minutes(self, minutes: int):
|
||||||
|
"""Set the valve to manual watering."""
|
||||||
|
self._manual_watering_minutes = minutes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watering_end_time(self) -> int:
|
||||||
|
"""Return the end time of the current watering cycle."""
|
||||||
|
return self._end_time
|
||||||
|
|
||||||
|
|
||||||
def mock_config_entry(hass: HomeAssistant):
|
def mock_config_entry(hass: HomeAssistant):
|
||||||
"""Return a mock config entry."""
|
"""Return a mock config entry."""
|
||||||
|
46
tests/components/melnor/test_number.py
Normal file
46
tests/components/melnor/test_number.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""Test the Melnor sensors."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .conftest import (
|
||||||
|
mock_config_entry,
|
||||||
|
patch_async_ble_device_from_address,
|
||||||
|
patch_async_register_callback,
|
||||||
|
patch_melnor_device,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_manual_watering_minutes(hass):
|
||||||
|
"""Test the manual watering switch."""
|
||||||
|
|
||||||
|
entry = mock_config_entry(hass)
|
||||||
|
|
||||||
|
with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback():
|
||||||
|
|
||||||
|
device = device_patch.return_value
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
number = hass.states.get("number.zone_1_manual_minutes")
|
||||||
|
|
||||||
|
print(number)
|
||||||
|
assert number.state == "0"
|
||||||
|
assert number.attributes["max"] == 360
|
||||||
|
assert number.attributes["min"] == 1
|
||||||
|
assert number.attributes["step"] == 1.0
|
||||||
|
assert number.attributes["icon"] == "mdi:timer-cog-outline"
|
||||||
|
|
||||||
|
assert device.zone1.manual_watering_minutes == 0
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
"number",
|
||||||
|
"set_value",
|
||||||
|
{"entity_id": "number.zone_1_manual_minutes", "value": 10},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
number = hass.states.get("number.zone_1_manual_minutes")
|
||||||
|
|
||||||
|
assert number.state == "10"
|
||||||
|
assert device.zone1.manual_watering_minutes == 10
|
@ -2,9 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||||
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
from homeassistant.const import PERCENTAGE, SIGNAL_STRENGTH_DECIBELS_MILLIWATT
|
||||||
from homeassistant.helpers import entity_registry
|
from homeassistant.helpers import entity_registry
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
mock_config_entry,
|
mock_config_entry,
|
||||||
@ -14,6 +17,8 @@ from .conftest import (
|
|||||||
patch_melnor_device,
|
patch_melnor_device,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from tests.common import async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
async def test_battery_sensor(hass):
|
async def test_battery_sensor(hass):
|
||||||
"""Test the battery sensor."""
|
"""Test the battery sensor."""
|
||||||
@ -31,6 +36,42 @@ async def test_battery_sensor(hass):
|
|||||||
assert battery_sensor.attributes["state_class"] == SensorStateClass.MEASUREMENT
|
assert battery_sensor.attributes["state_class"] == SensorStateClass.MEASUREMENT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_minutes_remaining_sensor(hass):
|
||||||
|
"""Test the minutes remaining sensor."""
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
|
||||||
|
entry = mock_config_entry(hass)
|
||||||
|
device = mock_melnor_device()
|
||||||
|
|
||||||
|
end_time = now + dt_util.dt.timedelta(minutes=10)
|
||||||
|
|
||||||
|
# we control this mock
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
device.zone1._end_time = (end_time).timestamp()
|
||||||
|
|
||||||
|
with freeze_time(now), patch_async_ble_device_from_address(), patch_melnor_device(
|
||||||
|
device
|
||||||
|
), patch_async_register_callback():
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Valve is off, report 0
|
||||||
|
minutes_sensor = hass.states.get("sensor.zone_1_manual_cycle_end")
|
||||||
|
assert minutes_sensor.state == "unknown"
|
||||||
|
assert minutes_sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP
|
||||||
|
|
||||||
|
# Turn valve on
|
||||||
|
device.zone1._is_watering = True
|
||||||
|
|
||||||
|
async_fire_time_changed(hass, now + dt_util.dt.timedelta(seconds=10))
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Valve is on, report 10
|
||||||
|
minutes_remaining_sensor = hass.states.get("sensor.zone_1_manual_cycle_end")
|
||||||
|
assert minutes_remaining_sensor.state == end_time.isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
async def test_rssi_sensor(hass):
|
async def test_rssi_sensor(hass):
|
||||||
"""Test the rssi sensor."""
|
"""Test the rssi sensor."""
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ async def test_manual_watering_switch_metadata(hass):
|
|||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1_manual")
|
switch = hass.states.get("switch.zone_1")
|
||||||
assert switch.attributes["device_class"] == SwitchDeviceClass.SWITCH
|
assert switch.attributes["device_class"] == SwitchDeviceClass.SWITCH
|
||||||
assert switch.attributes["icon"] == "mdi:sprinkler"
|
assert switch.attributes["icon"] == "mdi:sprinkler"
|
||||||
|
|
||||||
@ -32,30 +32,34 @@ async def test_manual_watering_switch_on_off(hass):
|
|||||||
|
|
||||||
entry = mock_config_entry(hass)
|
entry = mock_config_entry(hass)
|
||||||
|
|
||||||
with patch_async_ble_device_from_address(), patch_melnor_device(), patch_async_register_callback():
|
with patch_async_ble_device_from_address(), patch_melnor_device() as device_patch, patch_async_register_callback():
|
||||||
|
|
||||||
|
device = device_patch.return_value
|
||||||
|
|
||||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1_manual")
|
switch = hass.states.get("switch.zone_1")
|
||||||
assert switch.state is STATE_OFF
|
assert switch.state is STATE_OFF
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"switch",
|
"switch",
|
||||||
"turn_on",
|
"turn_on",
|
||||||
{"entity_id": "switch.zone_1_manual"},
|
{"entity_id": "switch.zone_1"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1_manual")
|
switch = hass.states.get("switch.zone_1")
|
||||||
assert switch.state is STATE_ON
|
assert switch.state is STATE_ON
|
||||||
|
assert device.zone1.is_watering is True
|
||||||
|
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
"switch",
|
"switch",
|
||||||
"turn_off",
|
"turn_off",
|
||||||
{"entity_id": "switch.zone_1_manual"},
|
{"entity_id": "switch.zone_1"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
switch = hass.states.get("switch.zone_1_manual")
|
switch = hass.states.get("switch.zone_1")
|
||||||
assert switch.state is STATE_OFF
|
assert switch.state is STATE_OFF
|
||||||
|
assert device.zone1.is_watering is False
|
||||||
|
Loading…
x
Reference in New Issue
Block a user